@fastagent-sh/fastagent 0.29.0 → 0.30.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -4
- package/dist/atomic-write.d.ts +4 -5
- package/dist/atomic-write.js +4 -5
- package/dist/authoring.d.ts +32 -15
- package/dist/authoring.js +147 -127
- package/dist/channels/agentcore-protocol.d.ts +8 -3
- package/dist/channels/agentcore-protocol.js +3 -1
- package/dist/channels/agentcore-service.d.ts +1 -0
- package/dist/channels/agentcore-service.js +14 -4
- package/dist/channels/agentcore.d.ts +9 -0
- package/dist/channels/agentcore.js +29 -4
- package/dist/channels/context.d.ts +2 -0
- package/dist/channels/context.js +5 -1
- package/dist/channels/feishu/feishu-api.d.ts +2 -1
- package/dist/channels/feishu/feishu.d.ts +0 -3
- package/dist/channels/feishu/feishu.js +10 -10
- package/dist/channels/feishu/register-app.js +3 -5
- package/dist/channels/feishu/scaffold/channel.ts +32 -34
- package/dist/channels/feishu/scaffold/feishu-send.ts +5 -5
- package/dist/channels/feishu/scaffold/feishu-threads.ts +3 -3
- package/dist/channels/feishu/setup-mode.d.ts +2 -12
- package/dist/channels/feishu/setup-mode.js +2 -22
- package/dist/channels/feishu/setup.d.ts +12 -0
- package/dist/channels/feishu/setup.js +197 -0
- package/dist/channels/kit/abortable.d.ts +8 -0
- package/dist/channels/kit/abortable.js +22 -0
- package/dist/channels/lark/lark.d.ts +0 -3
- package/dist/channels/lark/lark.js +0 -5
- package/dist/channels/lark/scaffold/channel.ts +33 -35
- package/dist/channels/lark/scaffold/lark-send.ts +5 -5
- package/dist/channels/lark/scaffold/lark-threads.ts +3 -3
- package/dist/channels/lark/setup.d.ts +9 -0
- package/dist/channels/lark/setup.js +57 -0
- package/dist/channels/setup.d.ts +112 -0
- package/dist/channels/setup.js +31 -0
- package/dist/channels/slack/history.js +1 -1
- package/dist/channels/slack/manifest.d.ts +17 -7
- package/dist/channels/slack/manifest.js +12 -16
- package/dist/channels/slack/scaffold/channel.ts +44 -30
- package/dist/channels/slack/scaffold/slack-send.ts +8 -8
- package/dist/channels/slack/scaffold/slack-threads.ts +4 -4
- package/dist/channels/slack/setup.d.ts +7 -0
- package/dist/channels/slack/setup.js +119 -0
- package/dist/channels/slack/slack-api.d.ts +2 -0
- package/dist/channels/slack/slack-api.js +4 -0
- package/dist/channels/slack/slack.d.ts +21 -2
- package/dist/channels/slack/slack.js +398 -338
- package/dist/channels/slack/socket.d.ts +57 -0
- package/dist/channels/slack/socket.js +305 -0
- package/dist/channels/telegram/polling.d.ts +27 -0
- package/dist/channels/telegram/polling.js +105 -0
- package/dist/channels/telegram/scaffold/channel.ts +52 -32
- package/dist/channels/telegram/scaffold/telegram-send.ts +9 -9
- package/dist/channels/telegram/setup.d.ts +2 -0
- package/dist/channels/telegram/setup.js +42 -0
- package/dist/channels/telegram/telegram-api.d.ts +27 -1
- package/dist/channels/telegram/telegram-api.js +7 -6
- package/dist/channels/telegram/telegram.d.ts +11 -3
- package/dist/channels/telegram/telegram.js +184 -157
- package/dist/channels/webhook-evidence.d.ts +11 -0
- package/dist/channels/webhook-evidence.js +71 -0
- package/dist/channels/webhooks.d.ts +37 -0
- package/dist/channels/webhooks.js +70 -0
- package/dist/child-env.d.ts +16 -0
- package/dist/child-env.js +30 -0
- package/dist/cli/box-login.js +4 -1
- package/dist/cli/channel-app.d.ts +3 -0
- package/dist/cli/channel-app.js +35 -0
- package/dist/cli/channel-setup.d.ts +38 -0
- package/dist/cli/channel-setup.js +195 -0
- package/dist/cli/commands/add.d.ts +6 -14
- package/dist/cli/commands/add.js +45 -303
- package/dist/cli/commands/auth.js +8 -2
- package/dist/cli/commands/build.d.ts +5 -0
- package/dist/cli/commands/build.js +16 -0
- package/dist/cli/commands/channels.d.ts +19 -0
- package/dist/cli/commands/channels.js +230 -0
- package/dist/cli/commands/content.d.ts +7 -3
- package/dist/cli/commands/content.js +16 -10
- package/dist/cli/commands/deploy/agentcore.js +55 -23
- package/dist/cli/commands/deploy/channel-apps.d.ts +17 -0
- package/dist/cli/commands/deploy/channel-apps.js +30 -0
- package/dist/cli/commands/deploy/docker.js +32 -65
- package/dist/cli/commands/deploy/fly.js +28 -12
- package/dist/cli/commands/deploy/railway.js +38 -13
- package/dist/cli/commands/deploy/secrets-step.d.ts +19 -0
- package/dist/cli/commands/deploy/secrets-step.js +66 -0
- package/dist/cli/commands/deploy/shared.d.ts +16 -5
- package/dist/cli/commands/deploy/shared.js +25 -8
- package/dist/cli/commands/deploy.js +53 -60
- package/dist/cli/commands/dev.d.ts +2 -0
- package/dist/cli/commands/dev.js +15 -3
- package/dist/cli/commands/env.js +1 -1
- package/dist/cli/commands/info.d.ts +2 -0
- package/dist/cli/commands/info.js +60 -25
- package/dist/cli/commands/init.js +3 -1
- package/dist/cli/commands/invoke.d.ts +2 -0
- package/dist/cli/commands/invoke.js +20 -7
- package/dist/cli/commands/login.d.ts +0 -7
- package/dist/cli/commands/login.js +22 -10
- package/dist/cli/commands/logout.js +1 -1
- package/dist/cli/commands/mcp.js +14 -6
- package/dist/cli/commands/models.js +3 -2
- package/dist/cli/commands/schedules.d.ts +4 -2
- package/dist/cli/commands/schedules.js +14 -10
- package/dist/cli/commands/secrets.d.ts +14 -0
- package/dist/cli/commands/secrets.js +213 -0
- package/dist/cli/commands/start.d.ts +14 -14
- package/dist/cli/commands/start.js +46 -60
- package/dist/cli/content-view.d.ts +4 -6
- package/dist/cli/content-view.js +5 -11
- package/dist/cli/first-run-channels.d.ts +3 -0
- package/dist/cli/first-run-channels.js +63 -0
- package/dist/cli/instance.d.ts +41 -0
- package/dist/cli/instance.js +155 -0
- package/dist/cli/invoke-stream.d.ts +5 -2
- package/dist/cli/invoke-stream.js +7 -2
- package/dist/cli/kernel.js +3 -0
- package/dist/cli/program.js +282 -110
- package/dist/cli/serve.d.ts +4 -5
- package/dist/cli/serve.js +4 -7
- package/dist/cli/shared.d.ts +11 -2
- package/dist/cli/shared.js +32 -19
- package/dist/cli/ticket.d.ts +7 -0
- package/dist/cli/ticket.js +106 -30
- package/dist/content/declare.d.ts +31 -37
- package/dist/content/declare.js +59 -60
- package/dist/content/file.d.ts +23 -12
- package/dist/content/file.js +48 -35
- package/dist/content/git.d.ts +0 -10
- package/dist/content/git.js +5 -39
- package/dist/content/resolve.d.ts +18 -22
- package/dist/content/resolve.js +32 -66
- package/dist/content/source.d.ts +7 -8
- package/dist/content/source.js +10 -28
- package/dist/credentials-page.d.ts +26 -0
- package/dist/credentials-page.js +185 -0
- package/dist/declared-secrets.d.ts +10 -3
- package/dist/declared-secrets.js +8 -4
- package/dist/deploy/agentcore/destroy.d.ts +4 -1
- package/dist/deploy/agentcore/destroy.js +25 -1
- package/dist/deploy/agentcore/forwarder.js +1 -1
- package/dist/deploy/agentcore/plan.d.ts +7 -21
- package/dist/deploy/agentcore/plan.js +47 -49
- package/dist/deploy/agentcore/run.d.ts +18 -10
- package/dist/deploy/agentcore/run.js +93 -70
- package/dist/deploy/agentcore/runtime-secrets.d.ts +7 -0
- package/dist/deploy/agentcore/runtime-secrets.js +108 -0
- package/dist/deploy/agentcore/shell.js +12 -19
- package/dist/deploy/agentcore/sigv4.d.ts +7 -0
- package/dist/deploy/agentcore/sigv4.js +15 -0
- package/dist/deploy/agentcore/slr.d.ts +18 -0
- package/dist/deploy/agentcore/slr.js +27 -0
- package/dist/deploy/agentcore/store.d.ts +7 -0
- package/dist/deploy/agentcore/store.js +115 -0
- package/dist/deploy/api.d.ts +14 -14
- package/dist/deploy/api.js +26 -24
- package/dist/deploy/box-shell.d.ts +3 -1
- package/dist/deploy/build-context.d.ts +4 -4
- package/dist/deploy/build-context.js +11 -15
- package/dist/deploy/channel-ingress.d.ts +35 -9
- package/dist/deploy/channel-ingress.js +119 -22
- package/dist/deploy/container.d.ts +35 -10
- package/dist/deploy/container.js +79 -37
- package/dist/deploy/delivered-text.d.ts +13 -0
- package/dist/deploy/delivered-text.js +86 -0
- package/dist/deploy/docker/plan.d.ts +7 -8
- package/dist/deploy/docker/plan.js +31 -52
- package/dist/deploy/docker/run.d.ts +5 -43
- package/dist/deploy/docker/run.js +9 -84
- package/dist/deploy/file-store.d.ts +2 -0
- package/dist/deploy/file-store.js +17 -0
- package/dist/deploy/fly/plan.d.ts +3 -2
- package/dist/deploy/fly/plan.js +20 -9
- package/dist/deploy/fly/run.d.ts +18 -8
- package/dist/deploy/fly/run.js +24 -42
- package/dist/deploy/fly/store.d.ts +12 -0
- package/dist/deploy/fly/store.js +83 -0
- package/dist/deploy/hosts.d.ts +1 -2
- package/dist/deploy/hosts.js +1 -2
- package/dist/deploy/preflight.d.ts +18 -5
- package/dist/deploy/preflight.js +135 -63
- package/dist/deploy/railway/ignore.d.ts +8 -0
- package/dist/deploy/railway/ignore.js +57 -0
- package/dist/deploy/railway/plan.d.ts +1 -2
- package/dist/deploy/railway/plan.js +8 -6
- package/dist/deploy/railway/run.d.ts +21 -15
- package/dist/deploy/railway/run.js +32 -78
- package/dist/deploy/railway/store.d.ts +29 -0
- package/dist/deploy/railway/store.js +143 -0
- package/dist/deploy/runner.d.ts +5 -2
- package/dist/deploy/runner.js +4 -3
- package/dist/deploy/secret-store.d.ts +63 -0
- package/dist/deploy/secret-store.js +98 -0
- package/dist/deploy/secrets.d.ts +29 -46
- package/dist/deploy/secrets.js +49 -122
- package/dist/deploy/workspace.d.ts +6 -12
- package/dist/deploy/workspace.js +18 -94
- package/dist/dev-supervisor.js +6 -8
- package/dist/environment/declare.js +3 -2
- package/dist/environment/mise.d.ts +3 -1
- package/dist/environment/mise.js +4 -2
- package/dist/feishu.d.ts +1 -1
- package/dist/feishu.js +1 -1
- package/dist/harnesses/models/catalog.d.ts +1 -0
- package/dist/harnesses/models/catalog.js +1 -2
- package/dist/harnesses/models/environment.d.ts +7 -2
- package/dist/harnesses/models/environment.js +9 -5
- package/dist/harnesses/models/host.d.ts +0 -7
- package/dist/harnesses/models/host.js +10 -1
- package/dist/harnesses/models/login.d.ts +0 -8
- package/dist/harnesses/models/login.js +5 -4
- package/dist/harnesses/models/models.d.ts +7 -2
- package/dist/harnesses/models/models.js +28 -4
- package/dist/harnesses/pi-durable/agent.js +59 -38
- package/dist/harnesses/pi-durable/build.d.ts +12 -0
- package/dist/harnesses/pi-durable/build.js +270 -0
- package/dist/harnesses/pi-durable/built.d.ts +24 -0
- package/dist/harnesses/pi-durable/built.js +136 -0
- package/dist/harnesses/pi-durable/config.d.ts +81 -0
- package/dist/harnesses/pi-durable/config.js +266 -0
- package/dist/harnesses/pi-durable/define.d.ts +63 -13
- package/dist/harnesses/pi-durable/define.js +23 -2
- package/dist/harnesses/pi-durable/definition.d.ts +72 -0
- package/dist/harnesses/pi-durable/definition.js +143 -0
- package/dist/harnesses/pi-durable/events.d.ts +4 -3
- package/dist/harnesses/pi-durable/events.js +179 -136
- package/dist/harnesses/pi-durable/execution-env.d.ts +17 -0
- package/dist/harnesses/pi-durable/execution-env.js +11 -0
- package/dist/harnesses/pi-durable/mcp.js +1 -3
- package/dist/harnesses/pi-durable/open.d.ts +35 -37
- package/dist/harnesses/pi-durable/open.js +91 -153
- package/dist/harnesses/pi-durable/reads.d.ts +0 -1
- package/dist/harnesses/pi-durable/reads.js +1 -3
- package/dist/harnesses/pi-durable/registry.d.ts +4 -6
- package/dist/harnesses/pi-durable/registry.js +29 -26
- package/dist/harnesses/pi-durable/release.d.ts +2 -2
- package/dist/harnesses/pi-durable/release.js +10 -7
- package/dist/harnesses/pi-durable/serve.d.ts +91 -15
- package/dist/harnesses/pi-durable/serve.js +109 -47
- package/dist/harnesses/pi-durable/sessions.js +15 -12
- package/dist/harnesses/pi-durable/text-sources.d.ts +30 -0
- package/dist/harnesses/pi-durable/text-sources.js +55 -0
- package/dist/harnesses/pi-durable/text.d.ts +16 -4
- package/dist/harnesses/pi-durable/text.js +115 -59
- package/dist/harnesses/pi-durable/wake.js +8 -8
- package/dist/http/client.js +9 -3
- package/dist/http/contract.d.ts +3 -3
- package/dist/http/server.d.ts +1 -1
- package/dist/http/server.js +1 -1
- package/dist/index.d.ts +8 -5
- package/dist/index.js +4 -1
- package/dist/instance-name.d.ts +7 -0
- package/dist/instance-name.js +26 -0
- package/dist/lark.d.ts +1 -1
- package/dist/lark.js +1 -1
- package/dist/mcp/client.js +11 -2
- package/dist/mcp/config.d.ts +70 -18
- package/dist/mcp/config.js +136 -53
- package/dist/mcp/host.d.ts +5 -2
- package/dist/mcp/host.js +14 -6
- package/dist/mcp/sign-in.js +2 -2
- package/dist/mcp-api.d.ts +14 -0
- package/dist/paths.d.ts +22 -8
- package/dist/paths.js +44 -11
- package/dist/runtime-websocket.d.ts +14 -0
- package/dist/runtime-websocket.js +39 -0
- package/dist/scaffold/add-channel.d.ts +9 -16
- package/dist/scaffold/add-channel.js +13 -83
- package/dist/scaffold/templates/env.example +4 -5
- package/dist/scaffold/templates/fastagent.config.ts +20 -14
- package/dist/scaffold/templates/gitignore +9 -6
- package/dist/scaffold/templates.d.ts +6 -2
- package/dist/scaffold/templates.js +7 -2
- package/dist/scaffold/vendor-skill.js +4 -2
- package/dist/service.d.ts +44 -6
- package/dist/service.js +110 -10
- package/dist/serving-command.d.ts +10 -0
- package/dist/serving-command.js +1 -0
- package/dist/slack.d.ts +1 -1
- package/dist/slack.js +1 -1
- package/dist/strip-edges.d.ts +10 -0
- package/dist/strip-edges.js +22 -0
- package/dist/telegram.d.ts +1 -1
- package/dist/telegram.js +1 -1
- package/dist/tunnel.d.ts +0 -1
- package/dist/tunnel.js +2 -8
- package/dist/value-file.d.ts +11 -6
- package/dist/value-file.js +21 -9
- package/dist/version.d.ts +7 -1
- package/dist/version.js +9 -3
- package/package.json +6 -5
- package/dist/channels/lark/credentials-page.d.ts +0 -25
- package/dist/channels/lark/credentials-page.js +0 -206
- package/dist/channels/lark/onboard.d.ts +0 -52
- package/dist/channels/lark/onboard.js +0 -86
- package/dist/channels/slack/config-api.d.ts +0 -69
- package/dist/channels/slack/config-api.js +0 -162
- package/dist/channels/slack/onboard.d.ts +0 -40
- package/dist/channels/slack/onboard.js +0 -136
- package/dist/channels/slack/onboarding-state.d.ts +0 -31
- package/dist/channels/slack/onboarding-state.js +0 -64
- package/dist/channels/slack/register-webhook.d.ts +0 -12
- package/dist/channels/slack/register-webhook.js +0 -67
- package/dist/channels/slack/setup-server.d.ts +0 -32
- package/dist/channels/slack/setup-server.js +0 -246
- package/dist/cli/add-feishu.d.ts +0 -41
- package/dist/cli/add-feishu.js +0 -391
- package/dist/cli/add-slack.d.ts +0 -18
- package/dist/cli/add-slack.js +0 -216
- package/dist/harnesses/pi-durable/parked.d.ts +0 -18
- package/dist/harnesses/pi-durable/parked.js +0 -27
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
3
|
import { MAX_WEBHOOK_BODY_BYTES } from "../../channels/agentcore-limits.js";
|
|
4
4
|
import { SECRETS_DIRNAME } from "../../paths.js";
|
|
5
|
-
import { webhookKinds
|
|
5
|
+
import { webhookKinds } from "../../channels/webhooks.js";
|
|
6
|
+
import { webhookRunbook } from "../channel-ingress.js";
|
|
6
7
|
import { containerArtifacts } from "../container.js";
|
|
7
8
|
import { deploymentLoginCommand } from "../box-shell.js";
|
|
9
|
+
import { stripEdges } from "../../strip-edges.js";
|
|
8
10
|
function agentcoreTopology(input) {
|
|
9
11
|
return { webhooks: input.channels.some((channel) => channel.ingress === "webhook") };
|
|
10
12
|
}
|
|
@@ -19,7 +21,7 @@ export const SECRETS_DIR = `${MOUNT}/${SECRETS_DIRNAME}`;
|
|
|
19
21
|
export const DEFAULT_IDLE_TIMEOUT_SECONDS = 180;
|
|
20
22
|
/** One session's compute lifetime — AWS's own default. Both lifecycle fields cap at 1209600 (14 d). */
|
|
21
23
|
export const MAX_LIFETIME_SECONDS = 28800;
|
|
22
|
-
export const FORWARDER_FILE = "
|
|
24
|
+
export const FORWARDER_FILE = "agentcore/forwarder/index.js";
|
|
23
25
|
/**
|
|
24
26
|
* Where the forwarder puts the one-shot schedule for a wake-up. The id inside the name is minted in the
|
|
25
27
|
* container, so this prefix is the only handle anything outside has on the alarms a deployment has pending.
|
|
@@ -38,22 +40,12 @@ export function recurringSchedulePrefix(name) {
|
|
|
38
40
|
export function deploymentBucketName(name, account) {
|
|
39
41
|
return `fa-${name}-${account}`;
|
|
40
42
|
}
|
|
41
|
-
/**
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
*/
|
|
45
|
-
export const CARRIER_CHUNK_SIZE = 2000;
|
|
46
|
-
export const CARRIER_MAX_CHUNKS = 4;
|
|
47
|
-
/** The template's chunked carrier: CloudFormation parameter prefix → container env-var name. */
|
|
48
|
-
export const CARRIERS = [
|
|
49
|
-
{ param: "FastagentEnv", env: "FASTAGENT_ENV", what: "base64 JSON of the value file's variables" },
|
|
50
|
-
];
|
|
51
|
-
/** Chunk `i` (0-based) of a carrier: its parameter and env-var names. */
|
|
52
|
-
export function carrierChunk(carrier, i) {
|
|
53
|
-
return i === 0
|
|
54
|
-
? { param: carrier.param, env: carrier.env }
|
|
55
|
-
: { param: `${carrier.param}${i + 1}`, env: `${carrier.env}_${i + 1}` };
|
|
43
|
+
/** The Secrets Manager secret holding a deployment's values: one JSON object of NAME → value (agent-artifact.md §6.2). */
|
|
44
|
+
export function agentcoreSecretName(name) {
|
|
45
|
+
return `fastagent/${name}`;
|
|
56
46
|
}
|
|
47
|
+
/** The environment variable that tells the container which secret to read at its first invocation. */
|
|
48
|
+
export const SECRET_ARN_ENV = "FASTAGENT_SECRET_ARN";
|
|
57
49
|
/** The generated template's filename (namespaced under the kit in the agentDir layout). */
|
|
58
50
|
export const TEMPLATE_FILE = "agentcore.template.yaml";
|
|
59
51
|
/** The generated template's first-line marker. */
|
|
@@ -64,14 +56,11 @@ export function isGeneratedAgentcoreTemplate(content) {
|
|
|
64
56
|
}
|
|
65
57
|
/** Deployment base name from the agent directory's name — the ONE mapping used to find its stack later. */
|
|
66
58
|
export function agentcoreName(agentDirName) {
|
|
67
|
-
return (agentDirName
|
|
68
|
-
.toLowerCase()
|
|
69
|
-
.replace(/[^a-z0-9-]+/g, "-")
|
|
70
|
-
.replace(/^-+|-+$/g, "") || "agent");
|
|
59
|
+
return stripEdges(agentDirName.toLowerCase().replace(/[^a-z0-9-]+/g, "-"), (char) => char === "-") || "agent";
|
|
71
60
|
}
|
|
72
61
|
/** Runtime name (`[a-zA-Z][a-zA-Z0-9_]{0,47}`) from a dir basename. */
|
|
73
62
|
export function toRuntimeName(basename) {
|
|
74
|
-
const slug = basename.replace(/[^a-zA-Z0-9]+/g, "_")
|
|
63
|
+
const slug = stripEdges(basename.replace(/[^a-zA-Z0-9]+/g, "_"), (char) => char === "_");
|
|
75
64
|
return (/^[a-zA-Z]/.test(slug) ? slug : `agent_${slug || "fastagent"}`).slice(0, 48);
|
|
76
65
|
}
|
|
77
66
|
/** The forwarder Lambda's name — and therefore its log group, which AWS derives from it. */
|
|
@@ -119,7 +108,7 @@ export function ingressSessionId(name) {
|
|
|
119
108
|
}
|
|
120
109
|
/**
|
|
121
110
|
* The forwarder Lambda source — `forwarder.js` beside this file, the ONE text both the deployment package and the
|
|
122
|
-
* readable `
|
|
111
|
+
* readable `agentcore/forwarder/index.js` artifact are generated from.
|
|
123
112
|
*/
|
|
124
113
|
export function forwarderSource() {
|
|
125
114
|
return readFileSync(new URL("./forwarder.js", import.meta.url), "utf8");
|
|
@@ -129,30 +118,20 @@ function template(input, topology) {
|
|
|
129
118
|
const runtimeName = toRuntimeName(input.name);
|
|
130
119
|
const idleTimeout = input.idleTimeoutSeconds ?? DEFAULT_IDLE_TIMEOUT_SECONDS;
|
|
131
120
|
const forwarderFnArn = `!Sub arn:aws:lambda:\${AWS::Region}:\${AWS::AccountId}:function:fastagent-${input.name}-forwarder`;
|
|
132
|
-
// Secret env vars ride CFN NoEcho parameters.
|
|
133
121
|
const params = [
|
|
134
122
|
` ImageUri:`,
|
|
135
123
|
` Type: String`,
|
|
136
124
|
` Description: ECR image URI (linux/arm64) — <account>.dkr.ecr.<region>.amazonaws.com/<repo>:<tag>`,
|
|
137
125
|
];
|
|
138
|
-
params.push(` ForwarderBucket:`, ` Type: String`, ` Description: S3 bucket holding the forwarder deployment package (created outside this stack)`, ` ForwarderS3Key:`, ` Type: String`, ` Description: content-hashed key of the forwarder .zip in ForwarderBucket`);
|
|
126
|
+
params.push(` SecretArn:`, ` Type: String`, ` Description: ARN of the Secrets Manager secret holding the deployment's values (created outside this stack by \`fastagent secrets\`; the runtime reads it on its first invocation)`, ` ForwarderBucket:`, ` Type: String`, ` Description: S3 bucket holding the forwarder deployment package (created outside this stack)`, ` ForwarderS3Key:`, ` Type: String`, ` Description: content-hashed key of the forwarder .zip in ForwarderBucket`);
|
|
139
127
|
const envLines = [
|
|
140
128
|
` PORT: "8080"`, // the Runtime service contract's fixed port (config.http.port does not apply here)
|
|
141
129
|
` FASTAGENT_AGENTCORE: "1"`, // serve mounts /invocations + /ping, arms no resident cron
|
|
142
|
-
`
|
|
130
|
+
` FASTAGENT_ROOT: ${MOUNT}`,
|
|
143
131
|
` FASTAGENT_STATE_DIR: ${MOUNT}/.state`,
|
|
144
132
|
` FASTAGENT_SECRETS_DIR: ${SECRETS_DIR}`,
|
|
133
|
+
` ${SECRET_ARN_ENV}: !Ref SecretArn`,
|
|
145
134
|
];
|
|
146
|
-
// The carrier is chunked (env values max 2048 chars — see CARRIER_CHUNK_SIZE): N parameters, each riding its
|
|
147
|
-
// own env var; `start` reassembles them. ONE parameter set for every variable, so the template does not change
|
|
148
|
-
// when the value file gains a name.
|
|
149
|
-
for (const carrier of CARRIERS) {
|
|
150
|
-
for (let i = 0; i < CARRIER_MAX_CHUNKS; i++) {
|
|
151
|
-
const { param, env } = carrierChunk(carrier, i);
|
|
152
|
-
params.push(` ${param}:`, ` Type: String`, ` Default: ""`, ` NoEcho: true`, ` Description: ${carrier.what} carried by --run, chunk ${i + 1}/${CARRIER_MAX_CHUNKS} (env values cap at 2048 chars); empty = unused`);
|
|
153
|
-
envLines.push(` ${env}: !Ref ${param}`);
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
135
|
// IAM invocation permission does not prove forwarder origin. Internal envelopes need a secret.
|
|
157
136
|
params.push(` FastagentIngressSecret:`, ` Type: String`, ` NoEcho: true`, ` Description: shared secret authenticating forwarder→runtime envelopes (any random string; --run mints one)`);
|
|
158
137
|
envLines.push(` FASTAGENT_INGRESS_SECRET: !Ref FastagentIngressSecret`);
|
|
@@ -199,6 +178,9 @@ function template(input, topology) {
|
|
|
199
178
|
` - Effect: Allow # AgentCore workload identity (the platform mints one per runtime)`,
|
|
200
179
|
` Action: [bedrock-agentcore:GetWorkloadAccessToken]`,
|
|
201
180
|
` Resource: "*"`,
|
|
181
|
+
` - Effect: Allow # the deployment's values: this one secret, read on the first invocation`,
|
|
182
|
+
` Action: [secretsmanager:GetSecretValue]`,
|
|
183
|
+
` Resource: !Ref SecretArn`,
|
|
202
184
|
``,
|
|
203
185
|
` Runtime:`,
|
|
204
186
|
` Type: AWS::BedrockAgentCore::Runtime`,
|
|
@@ -251,7 +233,12 @@ export function planAgentcoreDeploy(input) {
|
|
|
251
233
|
`# Prereqs: AWS CLI v2 with credentials + a region where AgentCore is available, and Docker with buildx`,
|
|
252
234
|
`# (the image MUST be linux/arm64 — the one host whose build runs on YOUR machine, not remotely).`,
|
|
253
235
|
``,
|
|
254
|
-
`#
|
|
236
|
+
`# 0. First use of AgentCore in this account only: the runtime needs its service-linked role, and without it`,
|
|
237
|
+
`# CloudFormation fails at the runtime, after the build, with a bare AccessDenied. Skip if \`aws iam get-role`,
|
|
238
|
+
`# --role-name AWSServiceRoleForBedrockAgentCoreRuntimeIdentity\` finds it.`,
|
|
239
|
+
`aws iam create-service-linked-role --aws-service-name runtime-identity.bedrock-agentcore.amazonaws.com`,
|
|
240
|
+
``,
|
|
241
|
+
`# 1. ECR repository, the forwarder artifact bucket and the deployment's values secret (one-time; skip what exists).`,
|
|
255
242
|
`aws ecr create-repository --repository-name ${repo}`,
|
|
256
243
|
`aws s3api create-bucket --bucket ${bucketHint} --region us-east-1 # us-east-1 ONLY`,
|
|
257
244
|
`aws s3api create-bucket --bucket ${bucketHint} --region <region> \\ # every OTHER region`,
|
|
@@ -259,10 +246,16 @@ export function planAgentcoreDeploy(input) {
|
|
|
259
246
|
`aws s3api put-public-access-block --bucket ${bucketHint} \\`,
|
|
260
247
|
` --public-access-block-configuration BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true`,
|
|
261
248
|
``,
|
|
249
|
+
`aws secretsmanager create-secret --name ${agentcoreSecretName(name)} --secret-string '{}'`,
|
|
250
|
+
...(secrets.length > 0
|
|
251
|
+
? [`# Its values, one command each (or \`fastagent secrets push agentcore\` for a file or the environment):`]
|
|
252
|
+
: []),
|
|
253
|
+
...secrets.map((s) => `# fastagent secrets set agentcore ${s.name} # ${s.hint}`),
|
|
254
|
+
``,
|
|
262
255
|
`# 1b. Package the forwarder and upload it. Name the object by its CONTENT (a hash/date):`,
|
|
263
256
|
`# CloudFormation rolls the function only when the ForwarderS3Key VALUE changes.`,
|
|
264
|
-
`(cd
|
|
265
|
-
`aws s3 cp
|
|
257
|
+
`(cd agentcore/forwarder && zip -q forwarder.zip index.js)`,
|
|
258
|
+
`aws s3 cp agentcore/forwarder/forwarder.zip s3://${bucketHint}/forwarder/<hash>.zip`,
|
|
266
259
|
``,
|
|
267
260
|
`# 2. Build (linux/arm64) + push. Use a UNIQUE tag per deploy (a git sha / date): CloudFormation only`,
|
|
268
261
|
`# rolls the runtime when the ImageUri VALUE changes — re-pushing the same tag deploys nothing.`,
|
|
@@ -271,19 +264,17 @@ export function planAgentcoreDeploy(input) {
|
|
|
271
264
|
``,
|
|
272
265
|
`# 3. Deploy the stack (runtime + ingress in one template).`,
|
|
273
266
|
];
|
|
274
|
-
|
|
275
|
-
runbook.push(`# Variables ride ONE NoEcho parameter, FastagentEnv: base64 of a JSON object {"NAME": "value", …},`, `# split every ${CARRIER_CHUNK_SIZE} chars across FastagentEnv, FastagentEnv2 … (\`--run\` builds it):`, ...secrets.map((s) => `# ${s.name}: ${s.hint}`));
|
|
276
|
-
}
|
|
267
|
+
runbook.push(`# SecretArn: the ARN of the secret made in step 1 (aws secretsmanager describe-secret --secret-id ${agentcoreSecretName(name)} --query ARN)`);
|
|
277
268
|
const wakeSecretHint = " FastagentWakeSecret=<any random string> FastagentIngressSecret=<another random string>";
|
|
278
269
|
runbook.push(`# FastagentWakeSecret: the wake-alarm shared secret — any random string (\`--run\` mints one)`);
|
|
279
270
|
runbook.push(`# FastagentIngressSecret: what proves an envelope came from the forwarder — another random string, kept for the activation below`);
|
|
280
|
-
runbook.push(`aws cloudformation deploy --stack-name ${stack} --template-file ${TEMPLATE_FILE} \\`, ` --capabilities CAPABILITY_IAM \\`, ` --parameter-overrides ImageUri=${image} ForwarderBucket=${bucketHint} ForwarderS3Key=forwarder/<hash>.zip${
|
|
271
|
+
runbook.push(`aws cloudformation deploy --stack-name ${stack} --template-file ${TEMPLATE_FILE} \\`, ` --capabilities CAPABILITY_IAM \\`, ` --parameter-overrides ImageUri=${image} SecretArn=<arn> ForwarderBucket=${bucketHint} ForwarderS3Key=forwarder/<hash>.zip${wakeSecretHint}`, ``, `# 4. Read the outputs (the runtime ARN + callback URL; it serves webhooks only when configured):`, `aws cloudformation describe-stacks --stack-name ${stack} --query "Stacks[0].Outputs"`, ``, `# 5. Tail the Runtime's application stdout/stderr (same fastagent messages + log level as locally).`, `# Discovery resolves the per-endpoint log group from the stack's RuntimeArn:`, `fastagent logs agentcore --follow`, `# The ingress transport is a separate Lambda and therefore a separate log source:`, `fastagent logs agentcore --source forwarder --follow`, ``, `# 6. Set a log retention period on EVERY group this stack writes to. Each is created by the service that`, `# writes it, not by this template, and CloudWatch keeps log data indefinitely by default — which is`, `# forever-billed storage, and the logs are where WHY a scheduled turn failed is recorded. Logs are the`, `# ONE state path this host does not reclaim on its own.`, `aws logs put-retention-policy --log-group-name <the group \`fastagent logs agentcore\` resolves> --retention-in-days 14`, `# The forwarder is a separate Lambda, so its group is separate and defaults the same way:`, `aws logs put-retention-policy --log-group-name ${forwarderLogGroup(input.name)} --retention-in-days 14`);
|
|
281
272
|
if (input.boxLogin) {
|
|
282
|
-
runbook.push(``, `# Model auth: after EVERY deploy and after 14 idle days (each storage reset wipes the last login),`, `# log the runtime in to ${input.boxLogin} from the agent directory
|
|
273
|
+
runbook.push(``, `# Model auth: after EVERY deploy and after 14 idle days (each storage reset wipes the last login),`, `# log the runtime in to ${input.boxLogin} from the agent directory, once the probe below has opened it (a message`, `# that reaches the runtime before the login fails):`, `${deploymentLoginCommand("agentcore", input.boxLogin)}`);
|
|
283
274
|
}
|
|
284
|
-
//
|
|
285
|
-
//
|
|
286
|
-
const post = webhookRunbook(`<ForwarderUrl>`, channels);
|
|
275
|
+
// The runtime points the channels its own credentials can at the forwarder's Function URL as the probe below wakes
|
|
276
|
+
// it; only Slack's URL, and Lark's where its config API answers 404, is a person's step.
|
|
277
|
+
const post = webhookRunbook(`<ForwarderUrl>`, channels.filter((channel) => channel.name === "slack"));
|
|
287
278
|
// AgentCore asides the shared steps cannot carry: nothing else routes through a forwarder, and nothing else has a
|
|
288
279
|
// compute ceiling.
|
|
289
280
|
if (webhookKinds(channels).some((kind) => kind === "feishu" || kind === "lark")) {
|
|
@@ -291,8 +282,15 @@ export function planAgentcoreDeploy(input) {
|
|
|
291
282
|
}
|
|
292
283
|
if (post.length > 0)
|
|
293
284
|
runbook.push(``, ...post);
|
|
294
|
-
|
|
285
|
+
const own = webhookKinds(channels).filter((kind) => kind !== "slack");
|
|
286
|
+
runbook.push(``, `# Schedules and wake-ups are EventBridge-backed: the container mirrors (via the forwarder, authenticated by`, `# FastagentWakeSecret) each schedule into a recurring EventBridge schedule (${recurringSchedulePrefix(name)}*) and each pending`, `# wake-up into a self-deleting one-shot (${wakeAlarmPrefix(name)}*), which wake it even when the compute is reclaimed.`, `# So a schedule written or edited on the runtime takes effect without a deploy. The container sets them only`, `# once an envelope has reached it THROUGH THE FORWARDER, which tells it where the forwarder is; an`, `# invoke-agent-runtime call does not (that door is IAM's, and does not carry it). \`--run\` does this with its`, `# probe. After a manual deploy, do it once, or no schedule fires until a webhook arrives:`, `curl -fsS -X POST "<ForwarderUrl>/__fastagent/probe" -d '{"auth":"<FastagentIngressSecret>"}'`, ...(own.length > 0
|
|
287
|
+
? [
|
|
288
|
+
`# The same envelope makes the runtime point ${own.join(", ")} at <ForwarderUrl> (or the configuration's`,
|
|
289
|
+
`# http.publicUrl) itself, on every start; the answer's "webhooks" says how each one went, "pending" until it ends`,
|
|
290
|
+
`# (a step only a person can take is in the runtime's log). Repeat the call to read it again.`,
|
|
291
|
+
]
|
|
292
|
+
: []));
|
|
295
293
|
runbook.push(``, `# Check that the deployed agent opens ({"ok":true}); it is reached through its channels, not this call:`, `aws bedrock-agentcore invoke-agent-runtime --agent-runtime-arn <RuntimeArn> \\`, ` --runtime-session-id "${ingressSessionId(name)}" \\`, ` --payload '${PROBE_PAYLOAD}' --cli-binary-format raw-in-base64-out /dev/stdout`);
|
|
296
|
-
runbook.push(``, `# Stop the fixed runtime session after redeploy so the next call uses the new image.`, `# \`--run\` does this automatically. In-flight turns are interrupted.`, `aws bedrock-agentcore stop-runtime-session --agent-runtime-arn <RuntimeArn> \\`, ` --runtime-session-id "${ingressSessionId(name)}"`, ``, `# Redeploy = \`fastagent deploy agentcore\` (a new definition needs a fresh release manifest) + step 1b`, `# (new forwarder key, if its code changed) + step 2 with a NEW tag + step 3.`, `# STATE: ${MOUNT} is managed SessionStorage. It keeps
|
|
294
|
+
runbook.push(``, `# Stop the fixed runtime session after redeploy so the next call uses the new image.`, `# \`--run\` does this automatically. In-flight turns are interrupted.`, `aws bedrock-agentcore stop-runtime-session --agent-runtime-arn <RuntimeArn> \\`, ` --runtime-session-id "${ingressSessionId(name)}"`, ``, `# Redeploy = \`fastagent deploy agentcore\` (a new definition needs a fresh release manifest) + step 1b`, `# (new forwarder key, if its code changed) + step 2 with a NEW tag + step 3.`, `# STATE: ${MOUNT} is managed SessionStorage, the agent's working directory. It keeps what the agent wrote there, .state/`, `# and .secrets/ across compute stop/resume — an idle-reclaimed agent resumes with its memory. AWS`, `# RESETS it on every runtime version update (i.e. every deploy) and after 14 idle days, so a deploy`, `# replaces the state along with the image: sessions, channel state and pending wake-ups start blank.`, `# A provider API key in the deployment's secret survives every deploy; a login on the runtime survives neither reset.`, `# Cross-deploy memory needs EFS or S3 Files, which are VPC-only (a NAT gateway for model/channel`, `# egress, ~$33/mo standing): use \`deploy fly\` or \`deploy railway\` for a real volume instead.`, `# Keep one runtime writer per agent; use the fixed runtime session id printed above for every entry point.`);
|
|
297
295
|
return { artifacts, runbook, topology };
|
|
298
296
|
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/** `fastagent deploy agentcore --run` — drive the AWS CLI + Docker to completion. */
|
|
2
2
|
import type { BoxLoginStep } from "../box-shell.ts";
|
|
3
3
|
import type { DeclaredChannel } from "../../channels/discover.ts";
|
|
4
|
-
import { type Registrars } from "../channel-ingress.ts";
|
|
4
|
+
import { type Registrars, type UrlRecorder } from "../channel-ingress.ts";
|
|
5
5
|
import type { CliRunner } from "../runner.ts";
|
|
6
6
|
import { type AgentcoreTopology } from "./plan.ts";
|
|
7
|
+
import type { SecretStore } from "../secret-store.ts";
|
|
7
8
|
export interface AgentcoreRunPlan {
|
|
8
9
|
/** The base name — stack `fastagent-<name>`, ECR repo `fastagent/<name>`. */
|
|
9
10
|
name: string;
|
|
@@ -18,12 +19,12 @@ export interface AgentcoreRunPlan {
|
|
|
18
19
|
* region`.
|
|
19
20
|
*/
|
|
20
21
|
region?: string;
|
|
21
|
-
/**
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
|
|
22
|
+
/** Makes the secret hold every value the deployment needs (secrets-step.ts), or says what is missing. */
|
|
23
|
+
provideSecrets: (store: SecretStore) => Promise<string | undefined>;
|
|
24
|
+
/** What proves an envelope came from the forwarder: minted by the deploy, a stack parameter. */
|
|
25
|
+
ingressSecret: string;
|
|
26
|
+
/** The wake-alarm shared secret between the container and the forwarder: minted by the deploy, a stack parameter. */
|
|
27
|
+
wakeSecret: string;
|
|
27
28
|
/** Every declared channel and its ingress — the driver asks which of them have a webhook. */
|
|
28
29
|
channels: readonly DeclaredChannel[];
|
|
29
30
|
/**
|
|
@@ -32,10 +33,14 @@ export interface AgentcoreRunPlan {
|
|
|
32
33
|
*/
|
|
33
34
|
topology: AgentcoreTopology;
|
|
34
35
|
/**
|
|
35
|
-
* Log the runtime in (`fastagent login --deployment`) once the probe verified it and before
|
|
36
|
-
*
|
|
36
|
+
* Log the runtime in (`fastagent login --deployment`) once the probe verified it and before the deploy reads its
|
|
37
|
+
* webhooks. Resolves a gate line, or undefined.
|
|
37
38
|
*/
|
|
38
39
|
boxLogin?: BoxLoginStep<[runtimeArn: string]>;
|
|
40
|
+
/** The `http.publicUrl` the configuration names (a custom domain); default the forwarder's URL. */
|
|
41
|
+
publicUrl?: string;
|
|
42
|
+
/** At a terminal with a `--config` file: record each URL a person set once the box confirms it. */
|
|
43
|
+
recordUrl?: UrlRecorder;
|
|
39
44
|
}
|
|
40
45
|
export type AgentcoreRunOutcome = {
|
|
41
46
|
ok: true;
|
|
@@ -57,7 +62,10 @@ export type AgentcoreRunOutcome = {
|
|
|
57
62
|
*/
|
|
58
63
|
export declare function pickStackOutputs(parsed: unknown): Record<string, string> | undefined;
|
|
59
64
|
/** The `--parameter-overrides file://` payload: a JSON array of "Key=Value" strings. */
|
|
60
|
-
export declare function paramsFileContent(imageUri: string,
|
|
65
|
+
export declare function paramsFileContent(imageUri: string, secretArn: string, minted: {
|
|
66
|
+
ingressSecret: string;
|
|
67
|
+
wakeSecret: string;
|
|
68
|
+
}, forwarder: {
|
|
61
69
|
bucket: string;
|
|
62
70
|
key: string;
|
|
63
71
|
}): string;
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { RESERVED_PATHS } from "../../channels/agentcore-protocol.js";
|
|
2
|
-
import { loginGate,
|
|
2
|
+
import { confirmWebhooks, loginGate, } from "../channel-ingress.js";
|
|
3
3
|
import { awsCli, awsJson } from "./aws-cli.js";
|
|
4
4
|
import { createHash } from "node:crypto";
|
|
5
5
|
import { Buffer } from "node:buffer";
|
|
6
|
-
import {
|
|
6
|
+
import { MOUNT, agentcoreRepoName, agentcoreSecretName, agentcoreStackName, deploymentBucketName, forwarderSource, ingressSessionId, } from "./plan.js";
|
|
7
7
|
import { zipSingleFile } from "./zip.js";
|
|
8
|
-
import {
|
|
8
|
+
import { RUNTIME_IDENTITY_ROLE, ensureServiceLinkedRole } from "./slr.js";
|
|
9
|
+
import { agentcoreSecretStore } from "./store.js";
|
|
9
10
|
/** AWS keys are typed, so a coding agent's user signs the CLI in at a terminal of their own. */
|
|
10
11
|
const AWS_SIGN_IN = "run `aws configure` (or `aws sso login`) in a terminal of your own, where the keys are typed, then re-run";
|
|
11
12
|
/**
|
|
@@ -23,23 +24,51 @@ const EMPTY_STACK_STATUSES = new Set([
|
|
|
23
24
|
"REVIEW_IN_PROGRESS",
|
|
24
25
|
"DELETE_COMPLETE",
|
|
25
26
|
]);
|
|
27
|
+
/**
|
|
28
|
+
* How often the deploy asks the runtime how its webhooks went. Each ask is a Lambda and an `InvokeAgentRuntime` that
|
|
29
|
+
* keeps the microVM up and is billed, where another host's `/health` costs nothing.
|
|
30
|
+
*/
|
|
31
|
+
const WEBHOOKS_INTERVAL_MS = 5_000;
|
|
26
32
|
/** Budget for image pull, storage initialization and channel construction. */
|
|
27
33
|
const PROBE_TIMEOUT_MS = 240_000;
|
|
28
34
|
const PROBE_INTERVAL_MS = 3_000;
|
|
35
|
+
/** One call to the forwarder's reserved probe path. */
|
|
36
|
+
async function askProbe(probeUrl, auth, fetchImpl) {
|
|
37
|
+
const res = await fetchImpl(probeUrl, {
|
|
38
|
+
method: "POST",
|
|
39
|
+
headers: { "content-type": "application/json" },
|
|
40
|
+
body: JSON.stringify({ auth }),
|
|
41
|
+
signal: AbortSignal.timeout(65_000),
|
|
42
|
+
});
|
|
43
|
+
return { status: res.status, body: await res.text() };
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The runtime's own report of its webhook channels, through the probe: the container has no `/health` of its own to
|
|
47
|
+
* read, and the forwarder relays only this path. The address it is handed is the other hosts' `/health`, which this
|
|
48
|
+
* one has no use for.
|
|
49
|
+
*/
|
|
50
|
+
function forwarderWebhooks(probeUrl, auth, fetchImpl) {
|
|
51
|
+
return async () => {
|
|
52
|
+
try {
|
|
53
|
+
const { status, body } = await askProbe(probeUrl, auth, fetchImpl);
|
|
54
|
+
const verdict = status === 200 ? JSON.parse(body) : undefined;
|
|
55
|
+
return verdict?.ok === true && Array.isArray(verdict.webhooks)
|
|
56
|
+
? verdict.webhooks
|
|
57
|
+
: undefined;
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return undefined; // polled again until the deadline
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
}
|
|
29
64
|
/** Drive the forwarder's reserved probe path until it answers, and read the runtime's STRUCTURED verdict. */
|
|
30
65
|
async function probeRuntime(probeUrl, auth, fetchImpl, timeoutMs = PROBE_TIMEOUT_MS, intervalMs = PROBE_INTERVAL_MS) {
|
|
31
66
|
const deadline = Date.now() + timeoutMs;
|
|
32
67
|
let last;
|
|
33
68
|
for (;;) {
|
|
34
69
|
try {
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
headers: { "content-type": "application/json" },
|
|
38
|
-
body: JSON.stringify({ auth }),
|
|
39
|
-
signal: AbortSignal.timeout(65_000),
|
|
40
|
-
});
|
|
41
|
-
const bodyText = await res.text();
|
|
42
|
-
if (res.status === 200) {
|
|
70
|
+
const { status, body: bodyText } = await askProbe(probeUrl, auth, fetchImpl);
|
|
71
|
+
if (status === 200) {
|
|
43
72
|
let verdict;
|
|
44
73
|
try {
|
|
45
74
|
verdict = JSON.parse(bodyText);
|
|
@@ -55,7 +84,7 @@ async function probeRuntime(probeUrl, auth, fetchImpl, timeoutMs = PROBE_TIMEOUT
|
|
|
55
84
|
}
|
|
56
85
|
}
|
|
57
86
|
const firstLine = bodyText.trim().split("\n")[0] ?? "";
|
|
58
|
-
last = `${
|
|
87
|
+
last = `${status}${firstLine ? ` ${firstLine}` : ""}`;
|
|
59
88
|
}
|
|
60
89
|
catch {
|
|
61
90
|
// not routable yet (Function URL DNS, cold start) — keep polling until the deadline
|
|
@@ -93,32 +122,16 @@ export function pickStackOutputs(parsed) {
|
|
|
93
122
|
}
|
|
94
123
|
return out;
|
|
95
124
|
}
|
|
96
|
-
/** The secrets `--run` mints for this host, each with the template parameter of its own. */
|
|
97
|
-
const MINTED_PARAMS = {
|
|
98
|
-
FASTAGENT_INGRESS_SECRET: "FastagentIngressSecret",
|
|
99
|
-
FASTAGENT_WAKE_SECRET: "FastagentWakeSecret",
|
|
100
|
-
};
|
|
101
|
-
/** What each carrier holds for this deploy: every variable but the minted ones, as one encoded object. */
|
|
102
|
-
function carrierValues(secrets) {
|
|
103
|
-
const carried = Object.fromEntries(Object.entries(secrets).filter(([name]) => !(name in MINTED_PARAMS)));
|
|
104
|
-
return { FASTAGENT_ENV: encodeCarriedEnv(carried) };
|
|
105
|
-
}
|
|
106
125
|
/** The `--parameter-overrides file://` payload: a JSON array of "Key=Value" strings. */
|
|
107
|
-
export function paramsFileContent(imageUri,
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
const value = values[carrier.env];
|
|
117
|
-
for (let i = 0; i < CARRIER_MAX_CHUNKS; i++) {
|
|
118
|
-
params.push(`${carrierChunk(carrier, i).param}=${value.slice(i * CARRIER_CHUNK_SIZE, (i + 1) * CARRIER_CHUNK_SIZE)}`);
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
return `${JSON.stringify(params)}\n`;
|
|
126
|
+
export function paramsFileContent(imageUri, secretArn, minted, forwarder) {
|
|
127
|
+
return `${JSON.stringify([
|
|
128
|
+
`ImageUri=${imageUri}`,
|
|
129
|
+
`SecretArn=${secretArn}`,
|
|
130
|
+
`ForwarderBucket=${forwarder.bucket}`,
|
|
131
|
+
`ForwarderS3Key=${forwarder.key}`,
|
|
132
|
+
`FastagentIngressSecret=${minted.ingressSecret}`,
|
|
133
|
+
`FastagentWakeSecret=${minted.wakeSecret}`,
|
|
134
|
+
])}\n`;
|
|
122
135
|
}
|
|
123
136
|
/** Run the deploy through `aws` + `docker`. */
|
|
124
137
|
export async function deployAgentcoreRun(plan, aws, docker, log, writeSecretFile, writeForwarderZip, registrars,
|
|
@@ -163,21 +176,6 @@ probe = {}) {
|
|
|
163
176
|
if ((await docker(["buildx", "version"], { capture: true })).code !== 0) {
|
|
164
177
|
return gate("docker buildx not available — the image must be linux/arm64 (cross-built); install buildx, then re-run");
|
|
165
178
|
}
|
|
166
|
-
// 3. Gate missing required secret VALUES before any side effect (no half-created infra).
|
|
167
|
-
const missingValues = missingValuesGate(plan.missingSecrets, plan.valueFile);
|
|
168
|
-
if (missingValues)
|
|
169
|
-
return gate(missingValues);
|
|
170
|
-
// 3b.
|
|
171
|
-
const capacity = CARRIER_CHUNK_SIZE * CARRIER_MAX_CHUNKS;
|
|
172
|
-
const carried = carrierValues(plan.secrets);
|
|
173
|
-
if (carried.FASTAGENT_ENV.length > capacity) {
|
|
174
|
-
return gate(`the variables in ${plan.valueFile} are too large to carry (${carried.FASTAGENT_ENV.length} chars encoded > ` +
|
|
175
|
-
`${capacity}) — move what the deployment does not need out of that file`);
|
|
176
|
-
}
|
|
177
|
-
// Name what travels from the value file onto the runtime.
|
|
178
|
-
const secretNames = Object.keys(plan.secrets);
|
|
179
|
-
if (secretNames.length > 0)
|
|
180
|
-
log(`carrying ${secretNames.length} secret(s): ${secretNames.join(", ")}`);
|
|
181
179
|
// 3c. Say what this deploy is ABOUT TO TOUCH while it can still be stopped for free: until here the account and
|
|
182
180
|
// region only surfaced several hundred log lines in, inside the ECR image URI.
|
|
183
181
|
const source = plan.region ? "the environment" : "aws configure";
|
|
@@ -203,7 +201,12 @@ probe = {}) {
|
|
|
203
201
|
`cloudformation deploy fails minutes from now, after the image build: ` +
|
|
204
202
|
`https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/agentcore-regions.html`);
|
|
205
203
|
}
|
|
206
|
-
// 3d.
|
|
204
|
+
// 3d. The role the runtime needs in this account, made if a first deploy finds it missing: a stop here costs nothing,
|
|
205
|
+
// and the same fault found by CloudFormation costs the build.
|
|
206
|
+
const roleGate = await ensureServiceLinkedRole(cli, RUNTIME_IDENTITY_ROLE, log);
|
|
207
|
+
if (roleGate !== undefined)
|
|
208
|
+
return gate(roleGate);
|
|
209
|
+
// 3e. Read the stack here, for the two questions that both depend on it: does this deploy destroy the agent's
|
|
207
210
|
// memory (below), and is there a failed first create to clear (step 7)? The destructive one is only worth saying
|
|
208
211
|
// while the multi-minute build has not run yet, and step 7 reuses this answer unless it was still in flight.
|
|
209
212
|
// An unanswered question must not read as "a first deploy, nothing to lose": `sts get-caller-identity` succeeding
|
|
@@ -236,6 +239,18 @@ probe = {}) {
|
|
|
236
239
|
`sessions, channel state and pending wake-ups start blank. ` +
|
|
237
240
|
`Cross-deploy memory needs a real volume: \`deploy fly\` or \`deploy railway\`.`);
|
|
238
241
|
}
|
|
242
|
+
// 3f. The deployment's values, after the only questions that precede a write and before the build: the secret the stack reads, made to hold every name the definition
|
|
243
|
+
// requires (a coding agent's run, with nobody to ask, stops here naming the commands).
|
|
244
|
+
const secretStore = agentcoreSecretStore(aws, agentcoreSecretName(plan.name), log);
|
|
245
|
+
try {
|
|
246
|
+
await secretStore.ensure();
|
|
247
|
+
}
|
|
248
|
+
catch (error) {
|
|
249
|
+
return gate(error instanceof Error ? error.message : String(error));
|
|
250
|
+
}
|
|
251
|
+
const secretsGate = await plan.provideSecrets(secretStore);
|
|
252
|
+
if (secretsGate !== undefined)
|
|
253
|
+
return gate(secretsGate);
|
|
239
254
|
// 4.
|
|
240
255
|
const registry = `${account}.dkr.ecr.${region}.amazonaws.com`;
|
|
241
256
|
const image = `${registry}/${repo}:${plan.tag}`;
|
|
@@ -306,7 +321,7 @@ probe = {}) {
|
|
|
306
321
|
if ((await docker(buildArgs)).code !== 0) {
|
|
307
322
|
return gate("`docker buildx build` failed — see the output above; fix and re-run");
|
|
308
323
|
}
|
|
309
|
-
// 7. The
|
|
324
|
+
// 7. The 3e answer stands unless it was still settling then — a second read is the narrow exception, not the rule.
|
|
310
325
|
const beforeDeploy = settling ? await readStackStatus() : stackStatus;
|
|
311
326
|
if ("ok" in beforeDeploy && beforeDeploy.ok === "ROLLBACK_COMPLETE") {
|
|
312
327
|
log(`stack ${stack} is ROLLBACK_COMPLETE (a failed first create) — deleting it before re-creating…`);
|
|
@@ -318,7 +333,14 @@ probe = {}) {
|
|
|
318
333
|
}
|
|
319
334
|
}
|
|
320
335
|
log(`deploying stack ${stack}…`);
|
|
321
|
-
|
|
336
|
+
let secretArn;
|
|
337
|
+
try {
|
|
338
|
+
secretArn = await secretStore.arn();
|
|
339
|
+
}
|
|
340
|
+
catch (error) {
|
|
341
|
+
return gate(error instanceof Error ? error.message : String(error));
|
|
342
|
+
}
|
|
343
|
+
const paramsPath = await writeSecretFile(paramsFileContent(image, secretArn, plan, forwarderParams));
|
|
322
344
|
const deployed = await aws([
|
|
323
345
|
"cloudformation",
|
|
324
346
|
"deploy",
|
|
@@ -386,7 +408,7 @@ probe = {}) {
|
|
|
386
408
|
// Verify storage initialization and channel construction before registering webhooks.
|
|
387
409
|
if (url) {
|
|
388
410
|
log("probing the deployed runtime (storage initialization + channel construction)…");
|
|
389
|
-
const verdict = await probeRuntime(`${url}${RESERVED_PATHS.probe}`, plan.
|
|
411
|
+
const verdict = await probeRuntime(`${url}${RESERVED_PATHS.probe}`, plan.ingressSecret, probe.fetchImpl ?? fetch, probe.timeoutMs, probe.intervalMs);
|
|
390
412
|
if (!verdict.ok)
|
|
391
413
|
return gate(verdict.gate);
|
|
392
414
|
log("runtime verified (storage ready, channels constructed)");
|
|
@@ -397,22 +419,23 @@ probe = {}) {
|
|
|
397
419
|
notLoggedIn,
|
|
398
420
|
channels: plan.channels,
|
|
399
421
|
log,
|
|
400
|
-
// The
|
|
401
|
-
|
|
402
|
-
? { afterLogin: "they work again: the forwarder URL is unchanged, so nothing needs pointing" }
|
|
403
|
-
: { baseUrl: url, afterLogin: "point the channels by hand (a redeploy would wipe that login again)" }),
|
|
422
|
+
// The runtime points its own channels at the forwarder URL as the probe wakes it, login or not.
|
|
423
|
+
afterLogin: "they work again: the runtime has pointed its own channels, so nothing needs pointing",
|
|
404
424
|
}));
|
|
405
425
|
}
|
|
406
|
-
// 9.
|
|
407
|
-
const registrationGateMsg =
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
:
|
|
426
|
+
// 9. The runtime points its own channels as the probe wakes it; this reads what it made of them.
|
|
427
|
+
const registrationGateMsg = await confirmWebhooks({
|
|
428
|
+
baseUrl: url,
|
|
429
|
+
publicUrl: plan.publicUrl ?? url,
|
|
430
|
+
channels: plan.channels,
|
|
431
|
+
registrars,
|
|
432
|
+
log,
|
|
433
|
+
...(plan.recordUrl === undefined ? {} : { record: plan.recordUrl }),
|
|
434
|
+
retryHint: "re-run to retry registration (steps already done are skipped)",
|
|
435
|
+
probe: forwarderWebhooks(`${url}${RESERVED_PATHS.probe}`, plan.ingressSecret, probe.fetchImpl ?? fetch),
|
|
436
|
+
intervalMs: probe.intervalMs ?? WEBHOOKS_INTERVAL_MS,
|
|
437
|
+
...(probe.timeoutMs === undefined ? {} : { timeoutMs: probe.timeoutMs }),
|
|
438
|
+
});
|
|
416
439
|
if (registrationGateMsg)
|
|
417
440
|
return gate(registrationGateMsg);
|
|
418
441
|
return { ok: true, runtimeArn, url };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** The secret's JSON object, every value a string. */
|
|
2
|
+
export declare function readSecretValues(secretArn: string): Promise<Record<string, string>>;
|
|
3
|
+
/**
|
|
4
|
+
* Put the secret's values into `env`, unless the platform set the name itself (the platform's variables win, as they do
|
|
5
|
+
* over a value file anywhere). Without `FASTAGENT_SECRET_ARN` there is nothing to read.
|
|
6
|
+
*/
|
|
7
|
+
export declare function applyRuntimeSecrets(env?: NodeJS.ProcessEnv): Promise<void>;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the AgentCore container does with its deployment's values: read the Secrets Manager secret `FASTAGENT_SECRET_ARN`
|
|
3
|
+
* names and put each name it holds into the environment (agent-artifact.md §6.2).
|
|
4
|
+
*
|
|
5
|
+
* The runtime has no way to reference a secret from an environment variable, so the container reads it itself, on its
|
|
6
|
+
* first invocation. Its credentials are the execution role's, from the microVM's instance-metadata service (IMDSv2); the
|
|
7
|
+
* request is SigV4-signed with the signer `shell.ts` already uses, so no AWS SDK is installed in the image.
|
|
8
|
+
*/
|
|
9
|
+
import { SignatureV4 } from "@smithy/signature-v4";
|
|
10
|
+
import { SECRET_ARN_ENV } from "./plan.js";
|
|
11
|
+
import { Sha256 } from "./sigv4.js";
|
|
12
|
+
const IMDS = "http://169.254.169.254/latest";
|
|
13
|
+
const TIMEOUT_MS = 5_000;
|
|
14
|
+
async function imdsText(url, init) {
|
|
15
|
+
const response = await fetch(url, { ...init, signal: AbortSignal.timeout(TIMEOUT_MS) });
|
|
16
|
+
if (!response.ok)
|
|
17
|
+
throw new Error(`${init.method ?? "GET"} ${url} answered ${response.status}`);
|
|
18
|
+
return response.text();
|
|
19
|
+
}
|
|
20
|
+
/** The execution role's credentials, as the metadata service hands them to the microVM. */
|
|
21
|
+
async function roleCredentials() {
|
|
22
|
+
const token = await imdsText(`${IMDS}/api/token`, {
|
|
23
|
+
method: "PUT",
|
|
24
|
+
headers: { "x-aws-ec2-metadata-token-ttl-seconds": "60" },
|
|
25
|
+
});
|
|
26
|
+
const headers = { "x-aws-ec2-metadata-token": token };
|
|
27
|
+
const role = (await imdsText(`${IMDS}/meta-data/iam/security-credentials/`, { headers })).trim();
|
|
28
|
+
const parsed = JSON.parse(await imdsText(`${IMDS}/meta-data/iam/security-credentials/${role}`, { headers }));
|
|
29
|
+
const { AccessKeyId, SecretAccessKey, Token } = (parsed ?? {});
|
|
30
|
+
if (typeof AccessKeyId !== "string" || typeof SecretAccessKey !== "string" || typeof Token !== "string") {
|
|
31
|
+
throw new Error(`the metadata service's credentials for ${role} are not an access key, a secret and a token`);
|
|
32
|
+
}
|
|
33
|
+
return { accessKeyId: AccessKeyId, secretAccessKey: SecretAccessKey, sessionToken: Token };
|
|
34
|
+
}
|
|
35
|
+
/** `arn:<partition>:secretsmanager:<region>:<account>:secret:<name>-<suffix>` → its region. */
|
|
36
|
+
function regionOf(secretArn) {
|
|
37
|
+
const [, , service, region] = secretArn.split(":");
|
|
38
|
+
if (service !== "secretsmanager" || !region)
|
|
39
|
+
throw new Error(`${SECRET_ARN_ENV} is not a Secrets Manager ARN`);
|
|
40
|
+
return region;
|
|
41
|
+
}
|
|
42
|
+
/** The secret's JSON object, every value a string. */
|
|
43
|
+
export async function readSecretValues(secretArn) {
|
|
44
|
+
const region = regionOf(secretArn);
|
|
45
|
+
const hostname = `secretsmanager.${region}.amazonaws.com`;
|
|
46
|
+
const body = JSON.stringify({ SecretId: secretArn });
|
|
47
|
+
const signed = await new SignatureV4({
|
|
48
|
+
service: "secretsmanager",
|
|
49
|
+
region,
|
|
50
|
+
credentials: await roleCredentials(),
|
|
51
|
+
sha256: Sha256,
|
|
52
|
+
}).sign({
|
|
53
|
+
method: "POST",
|
|
54
|
+
protocol: "https:",
|
|
55
|
+
hostname,
|
|
56
|
+
path: "/",
|
|
57
|
+
body,
|
|
58
|
+
headers: {
|
|
59
|
+
host: hostname,
|
|
60
|
+
"content-type": "application/x-amz-json-1.1",
|
|
61
|
+
"x-amz-target": "secretsmanager.GetSecretValue",
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
const response = await fetch(`https://${hostname}/`, {
|
|
65
|
+
method: "POST",
|
|
66
|
+
headers: signed.headers,
|
|
67
|
+
body,
|
|
68
|
+
signal: AbortSignal.timeout(TIMEOUT_MS),
|
|
69
|
+
});
|
|
70
|
+
const answer = await response.text();
|
|
71
|
+
// Never the parser's message: it quotes the text around the error, and this document holds every value.
|
|
72
|
+
if (!response.ok)
|
|
73
|
+
throw new Error(`GetSecretValue answered ${response.status}: ${answer.slice(0, 200)}`);
|
|
74
|
+
let values;
|
|
75
|
+
try {
|
|
76
|
+
values = JSON.parse(JSON.parse(answer).SecretString ?? "");
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
throw new Error(`the secret ${secretArn} is not a JSON object of values`);
|
|
80
|
+
}
|
|
81
|
+
if (values === null || typeof values !== "object" || Array.isArray(values)) {
|
|
82
|
+
throw new Error(`the secret ${secretArn} is not a JSON object of values`);
|
|
83
|
+
}
|
|
84
|
+
for (const [name, value] of Object.entries(values)) {
|
|
85
|
+
if (typeof value !== "string")
|
|
86
|
+
throw new Error(`the secret ${secretArn}: ${name} is not a string`);
|
|
87
|
+
}
|
|
88
|
+
return values;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Put the secret's values into `env`, unless the platform set the name itself (the platform's variables win, as they do
|
|
92
|
+
* over a value file anywhere). Without `FASTAGENT_SECRET_ARN` there is nothing to read.
|
|
93
|
+
*/
|
|
94
|
+
export async function applyRuntimeSecrets(env = process.env) {
|
|
95
|
+
const arn = env[SECRET_ARN_ENV];
|
|
96
|
+
if (!arn)
|
|
97
|
+
return;
|
|
98
|
+
let values;
|
|
99
|
+
try {
|
|
100
|
+
values = await readSecretValues(arn);
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
throw new Error(`could not read the deployment's values from ${arn}: ${error instanceof Error ? error.message : String(error)}`);
|
|
104
|
+
}
|
|
105
|
+
for (const [name, value] of Object.entries(values))
|
|
106
|
+
if (!(name in env))
|
|
107
|
+
env[name] = value;
|
|
108
|
+
}
|