@fastagent-sh/fastagent 0.21.0 → 0.22.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 +9 -12
- package/dist/agent.d.ts +24 -37
- package/dist/agent.js +7 -15
- package/dist/atomic-write.d.ts +13 -17
- package/dist/atomic-write.js +18 -29
- package/dist/bind.d.ts +12 -22
- package/dist/bind.js +18 -27
- package/dist/channel.d.ts +11 -33
- package/dist/channels/agentcore-limits.d.ts +2 -13
- package/dist/channels/agentcore-limits.js +2 -13
- package/dist/channels/agentcore-protocol.d.ts +67 -58
- package/dist/channels/agentcore-protocol.js +23 -15
- package/dist/channels/agentcore-service.d.ts +19 -33
- package/dist/channels/agentcore-service.js +176 -59
- package/dist/channels/agentcore.d.ts +39 -33
- package/dist/channels/agentcore.js +90 -224
- package/dist/channels/body.d.ts +15 -2
- package/dist/channels/body.js +24 -2
- package/dist/channels/busy.d.ts +1 -21
- package/dist/channels/busy.js +2 -39
- package/dist/channels/control.d.ts +17 -62
- package/dist/channels/control.js +74 -182
- package/dist/channels/define-channel.d.ts +39 -0
- package/dist/channels/define-channel.js +9 -0
- package/dist/channels/discover.d.ts +10 -31
- package/dist/channels/discover.js +37 -31
- package/dist/channels/feishu/bootstrap-token.d.ts +1 -8
- package/dist/channels/feishu/bootstrap-token.js +7 -23
- package/dist/channels/feishu/card.d.ts +14 -28
- package/dist/channels/feishu/card.js +16 -30
- package/dist/channels/feishu/cloud.d.ts +1 -5
- package/dist/channels/feishu/context-buffer.d.ts +4 -23
- package/dist/channels/feishu/context-buffer.js +8 -31
- package/dist/channels/feishu/crypto.d.ts +10 -5
- package/dist/channels/feishu/crypto.js +12 -19
- package/dist/channels/feishu/feishu-api.d.ts +2 -1
- package/dist/channels/feishu/feishu-api.js +5 -4
- package/dist/channels/feishu/feishu.d.ts +4 -10
- package/dist/channels/feishu/feishu.js +113 -227
- package/dist/channels/feishu/invoke-turn.d.ts +5 -4
- package/dist/channels/feishu/invoke-turn.js +36 -54
- package/dist/channels/feishu/model.d.ts +4 -21
- package/dist/channels/feishu/model.js +1 -8
- package/dist/channels/feishu/normalize.d.ts +2 -10
- package/dist/channels/feishu/normalize.js +7 -42
- package/dist/channels/feishu/parse.d.ts +9 -29
- package/dist/channels/feishu/parse.js +8 -26
- package/dist/channels/feishu/preview.d.ts +12 -14
- package/dist/channels/feishu/preview.js +108 -207
- package/dist/channels/feishu/register-app.d.ts +8 -23
- package/dist/channels/feishu/register-app.js +7 -20
- package/dist/channels/feishu/register-webhook.d.ts +2 -9
- package/dist/channels/feishu/register-webhook.js +9 -33
- package/dist/channels/feishu/scaffold/channel.ts +27 -19
- package/dist/channels/feishu/scaffold/channel.websocket.ts +31 -0
- package/dist/channels/feishu/setup-mode.d.ts +11 -36
- package/dist/channels/feishu/setup-mode.js +7 -26
- package/dist/channels/feishu/ws-ingress.d.ts +1 -2
- package/dist/channels/feishu/ws-ingress.js +6 -8
- package/dist/channels/http.d.ts +15 -15
- package/dist/channels/http.js +19 -9
- package/dist/channels/kit/attachment-path.d.ts +4 -5
- package/dist/channels/kit/attachment-path.js +10 -34
- package/dist/channels/kit/context-buffer.d.ts +14 -21
- package/dist/channels/kit/context-buffer.js +25 -38
- package/dist/channels/kit/delivery.d.ts +35 -0
- package/dist/channels/kit/delivery.js +129 -0
- package/dist/channels/kit/event-stream.d.ts +5 -0
- package/dist/channels/kit/event-stream.js +29 -0
- package/dist/channels/kit/invoke-turn-kit.d.ts +53 -26
- package/dist/channels/kit/invoke-turn-kit.js +89 -32
- package/dist/channels/kit/preview-kit.d.ts +18 -72
- package/dist/channels/kit/preview-kit.js +22 -88
- package/dist/channels/kit/signature.d.ts +3 -9
- package/dist/channels/kit/signature.js +3 -9
- package/dist/channels/kit/state.d.ts +1 -2
- package/dist/channels/kit/state.js +5 -14
- package/dist/channels/kit/stop-command.d.ts +1 -2
- package/dist/channels/kit/stop-command.js +6 -9
- package/dist/channels/kit/tasks.d.ts +1 -1
- package/dist/channels/kit/tasks.js +8 -21
- package/dist/channels/kit/text.d.ts +2 -16
- package/dist/channels/kit/text.js +23 -31
- package/dist/channels/kit/thread-participants.d.ts +4 -17
- package/dist/channels/kit/thread-participants.js +8 -55
- package/dist/channels/kit/transport.d.ts +26 -0
- package/dist/channels/kit/transport.js +20 -0
- package/dist/channels/kit/turn-queue.d.ts +7 -11
- package/dist/channels/kit/turn-queue.js +21 -39
- package/dist/channels/kit/turn-runner.d.ts +32 -18
- package/dist/channels/kit/turn-runner.js +98 -53
- package/dist/channels/kit/turn-store.d.ts +32 -32
- package/dist/channels/kit/turn-store.js +45 -88
- package/dist/channels/lark/lark.d.ts +1 -5
- package/dist/channels/lark/onboard.d.ts +3 -12
- package/dist/channels/lark/onboard.js +3 -13
- package/dist/channels/lark/scaffold/channel.ts +27 -19
- package/dist/channels/lark/scaffold/channel.websocket.ts +31 -0
- package/dist/channels/registration.d.ts +9 -35
- package/dist/channels/registration.js +6 -37
- package/dist/channels/secret.js +2 -5
- package/dist/channels/serve.d.ts +47 -44
- package/dist/channels/serve.js +165 -101
- package/dist/channels/slack/config-api.d.ts +4 -27
- package/dist/channels/slack/config-api.js +5 -34
- package/dist/channels/slack/context-buffer.d.ts +2 -3
- package/dist/channels/slack/context-buffer.js +5 -5
- package/dist/channels/slack/invoke-turn.d.ts +3 -1
- package/dist/channels/slack/invoke-turn.js +25 -34
- package/dist/channels/slack/manifest.js +1 -3
- package/dist/channels/slack/model.d.ts +1 -2
- package/dist/channels/slack/model.js +1 -1
- package/dist/channels/slack/onboard.d.ts +4 -3
- package/dist/channels/slack/onboard.js +8 -11
- package/dist/channels/slack/onboarding-state.d.ts +3 -5
- package/dist/channels/slack/onboarding-state.js +9 -6
- package/dist/channels/slack/parse.d.ts +4 -13
- package/dist/channels/slack/parse.js +7 -28
- package/dist/channels/slack/preview.d.ts +12 -2
- package/dist/channels/slack/preview.js +292 -269
- package/dist/channels/slack/reaction.d.ts +11 -3
- package/dist/channels/slack/reaction.js +19 -8
- package/dist/channels/slack/register-webhook.js +4 -7
- package/dist/channels/slack/scaffold/channel.ts +20 -13
- package/dist/channels/slack/scaffold/slack-send.ts +2 -2
- package/dist/channels/slack/setup-server.d.ts +1 -5
- package/dist/channels/slack/setup-server.js +1 -5
- package/dist/channels/slack/shared-api.d.ts +1 -4
- package/dist/channels/slack/shared-api.js +2 -13
- package/dist/channels/slack/slack-api.d.ts +2 -1
- package/dist/channels/slack/slack-api.js +6 -4
- package/dist/channels/slack/slack.d.ts +11 -14
- package/dist/channels/slack/slack.js +121 -122
- package/dist/channels/sse.d.ts +1 -1
- package/dist/channels/sse.js +1 -1
- package/dist/channels/telegram/context-buffer.d.ts +7 -22
- package/dist/channels/telegram/context-buffer.js +6 -23
- package/dist/channels/telegram/invoke-turn.d.ts +7 -7
- package/dist/channels/telegram/invoke-turn.js +24 -43
- package/dist/channels/telegram/parse.d.ts +24 -51
- package/dist/channels/telegram/parse.js +32 -84
- package/dist/channels/telegram/preview.d.ts +10 -1
- package/dist/channels/telegram/preview.js +79 -100
- package/dist/channels/telegram/register-webhook.js +5 -23
- package/dist/channels/telegram/scaffold/channel.ts +25 -18
- package/dist/channels/telegram/scaffold/telegram-send.ts +6 -4
- package/dist/channels/telegram/telegram-api.d.ts +4 -3
- package/dist/channels/telegram/telegram-api.js +9 -7
- package/dist/channels/telegram/telegram.d.ts +4 -18
- package/dist/channels/telegram/telegram.js +38 -94
- package/dist/channels/telegram/turn-store.d.ts +11 -14
- package/dist/channels/telegram/turn-store.js +8 -6
- package/dist/channels/wait-health.d.ts +4 -6
- package/dist/channels/wait-health.js +10 -14
- package/dist/cli/add-feishu.d.ts +2 -12
- package/dist/cli/add-feishu.js +24 -71
- package/dist/cli/add-slack.d.ts +5 -3
- package/dist/cli/add-slack.js +53 -76
- package/dist/cli/auth-view.d.ts +2 -16
- package/dist/cli/auth-view.js +2 -16
- package/dist/cli/commands/add.js +25 -43
- package/dist/cli/commands/chat.d.ts +0 -1
- package/dist/cli/commands/chat.js +5 -6
- package/dist/cli/commands/deploy/agentcore.js +60 -65
- package/dist/cli/commands/deploy/docker.js +50 -23
- package/dist/cli/commands/deploy/fly.js +23 -61
- package/dist/cli/commands/deploy/railway.js +23 -18
- package/dist/cli/commands/deploy/shared.d.ts +38 -57
- package/dist/cli/commands/deploy/shared.js +40 -59
- package/dist/cli/commands/deploy.d.ts +10 -27
- package/dist/cli/commands/deploy.js +38 -60
- package/dist/cli/commands/destroy.d.ts +4 -0
- package/dist/cli/commands/destroy.js +56 -0
- package/dist/cli/commands/dev.d.ts +2 -1
- package/dist/cli/commands/dev.js +20 -23
- package/dist/cli/commands/info.d.ts +0 -2
- package/dist/cli/commands/info.js +83 -49
- package/dist/cli/commands/init.d.ts +1 -2
- package/dist/cli/commands/init.js +16 -55
- package/dist/cli/commands/invoke.d.ts +0 -1
- package/dist/cli/commands/invoke.js +5 -7
- package/dist/cli/commands/login.d.ts +2 -1
- package/dist/cli/commands/login.js +33 -38
- package/dist/cli/commands/logs.js +3 -4
- package/dist/cli/commands/models.js +1 -5
- package/dist/cli/commands/routine-run.d.ts +6 -0
- package/dist/cli/commands/routine-run.js +47 -0
- package/dist/cli/commands/routine.d.ts +25 -0
- package/dist/cli/commands/routine.js +122 -0
- package/dist/cli/commands/start.d.ts +31 -3
- package/dist/cli/commands/start.js +131 -81
- package/dist/cli/commands/tool.js +26 -12
- package/dist/cli/fail.d.ts +13 -23
- package/dist/cli/fail.js +20 -23
- package/dist/cli/invoke-stream.d.ts +1 -8
- package/dist/cli/kernel.d.ts +6 -38
- package/dist/cli/kernel.js +14 -77
- package/dist/cli/models-view.d.ts +1 -9
- package/dist/cli/models-view.js +11 -22
- package/dist/cli/program.d.ts +5 -6
- package/dist/cli/program.js +120 -157
- package/dist/cli/serve.d.ts +57 -41
- package/dist/cli/serve.js +94 -121
- package/dist/cli/shared.d.ts +15 -61
- package/dist/cli/shared.js +55 -181
- package/dist/cli.js +1 -8
- package/dist/collect.d.ts +6 -0
- package/dist/collect.js +6 -0
- package/dist/core.d.ts +4 -1
- package/dist/core.js +9 -7
- package/dist/declared-secrets.d.ts +55 -0
- package/dist/declared-secrets.js +88 -0
- package/dist/deploy/agentcore/aws-cli.d.ts +91 -0
- package/dist/deploy/agentcore/aws-cli.js +64 -0
- package/dist/deploy/agentcore/destroy.d.ts +51 -0
- package/dist/deploy/agentcore/destroy.js +273 -0
- package/dist/deploy/agentcore/forwarder.js +28 -79
- package/dist/deploy/agentcore/logs.d.ts +2 -18
- package/dist/deploy/agentcore/logs.js +27 -51
- package/dist/deploy/agentcore/plan.d.ts +85 -105
- package/dist/deploy/agentcore/plan.js +152 -314
- package/dist/deploy/agentcore/run.d.ts +28 -28
- package/dist/deploy/agentcore/run.js +227 -306
- package/dist/deploy/agentcore/zip.d.ts +2 -15
- package/dist/deploy/agentcore/zip.js +3 -17
- package/dist/deploy/build-context.d.ts +21 -0
- package/dist/deploy/build-context.js +137 -0
- package/dist/deploy/channel-ingress.d.ts +26 -39
- package/dist/deploy/channel-ingress.js +57 -28
- package/dist/deploy/container.d.ts +20 -34
- package/dist/deploy/container.js +49 -100
- package/dist/deploy/docker/plan.d.ts +12 -11
- package/dist/deploy/docker/plan.js +43 -24
- package/dist/deploy/docker/run.d.ts +22 -25
- package/dist/deploy/docker/run.js +39 -37
- package/dist/deploy/fly/plan.d.ts +15 -44
- package/dist/deploy/fly/plan.js +41 -73
- package/dist/deploy/fly/run.d.ts +16 -51
- package/dist/deploy/fly/run.js +42 -95
- package/dist/deploy/hosts.d.ts +24 -3
- package/dist/deploy/hosts.js +24 -3
- package/dist/deploy/preflight.d.ts +47 -41
- package/dist/deploy/preflight.js +282 -383
- package/dist/deploy/railway/plan.d.ts +16 -42
- package/dist/deploy/railway/plan.js +42 -60
- package/dist/deploy/railway/run.d.ts +30 -68
- package/dist/deploy/railway/run.js +86 -100
- package/dist/deploy/registration-gate.d.ts +6 -13
- package/dist/deploy/residency.d.ts +37 -0
- package/dist/deploy/residency.js +24 -0
- package/dist/deploy/runner.d.ts +10 -9
- package/dist/deploy/runner.js +7 -18
- package/dist/deploy/secrets.d.ts +40 -41
- package/dist/deploy/secrets.js +117 -103
- package/dist/deploy/workspace.d.ts +50 -0
- package/dist/deploy/workspace.js +197 -0
- package/dist/dev-supervisor.d.ts +1 -9
- package/dist/dev-supervisor.js +21 -64
- package/dist/effect-port.d.ts +50 -0
- package/dist/effect-port.js +77 -0
- package/dist/engines/pi/agent-session-factory.d.ts +50 -69
- package/dist/engines/pi/agent-session-factory.js +249 -146
- package/dist/engines/pi/auth.d.ts +15 -11
- package/dist/engines/pi/auth.js +96 -99
- package/dist/engines/pi/chat.d.ts +1 -5
- package/dist/engines/pi/chat.js +2 -14
- package/dist/engines/pi/config.d.ts +53 -51
- package/dist/engines/pi/config.js +83 -92
- package/dist/engines/pi/create.d.ts +60 -75
- package/dist/engines/pi/create.js +108 -141
- package/dist/engines/pi/definition.d.ts +14 -58
- package/dist/engines/pi/definition.js +21 -85
- package/dist/engines/pi/invoke-session.d.ts +5 -25
- package/dist/engines/pi/invoke-session.js +209 -244
- package/dist/engines/pi/login.d.ts +95 -35
- package/dist/engines/pi/login.js +259 -107
- package/dist/engines/pi/machine.d.ts +31 -0
- package/dist/engines/pi/machine.js +106 -0
- package/dist/engines/pi/models.d.ts +70 -89
- package/dist/engines/pi/models.js +188 -81
- package/dist/engines/pi/open.d.ts +65 -53
- package/dist/engines/pi/open.js +129 -69
- package/dist/engines/pi/report.d.ts +3 -12
- package/dist/engines/pi/report.js +2 -16
- package/dist/engines/pi/search-tools.d.ts +1 -9
- package/dist/engines/pi/search-tools.js +26 -57
- package/dist/engines/pi/service.d.ts +2 -16
- package/dist/engines/pi/service.js +2 -16
- package/dist/engines/pi/session-builder.d.ts +5 -9
- package/dist/engines/pi/session-builder.js +44 -82
- package/dist/engines/pi/session-control.js +286 -269
- package/dist/engines/pi/session-effects.d.ts +14 -0
- package/dist/engines/pi/session-effects.js +30 -0
- package/dist/engines/pi/session-inheritance.d.ts +9 -18
- package/dist/engines/pi/session-inheritance.js +103 -160
- package/dist/engines/pi/session-markers.d.ts +31 -30
- package/dist/engines/pi/session-markers.js +26 -25
- package/dist/engines/pi/session-settings.d.ts +6 -22
- package/dist/engines/pi/session-settings.js +10 -21
- package/dist/engines/pi/session-store.d.ts +26 -92
- package/dist/engines/pi/session-store.js +96 -310
- package/dist/engines/pi/tool-context.d.ts +17 -45
- package/dist/engines/pi/tool-context.js +7 -26
- package/dist/engines/pi/tool.d.ts +45 -36
- package/dist/engines/pi/tool.js +43 -57
- package/dist/engines/pi/turn-kit.d.ts +14 -52
- package/dist/engines/pi/turn-kit.js +18 -67
- package/dist/engines/pi/wake-tool.d.ts +2 -10
- package/dist/engines/pi/wake-tool.js +7 -27
- package/dist/env.d.ts +27 -28
- package/dist/env.js +45 -50
- package/dist/index.js +0 -2
- package/dist/lark.d.ts +1 -2
- package/dist/lark.js +1 -2
- package/dist/loader.d.ts +14 -55
- package/dist/loader.js +24 -43
- package/dist/log.d.ts +2 -12
- package/dist/log.js +3 -24
- package/dist/node.d.ts +2 -15
- package/dist/node.js +2 -18
- package/dist/observe.d.ts +2 -8
- package/dist/open-url.d.ts +1 -1
- package/dist/open-url.js +1 -1
- package/dist/paths.d.ts +70 -134
- package/dist/paths.js +165 -232
- package/dist/pi.d.ts +4 -6
- package/dist/pi.js +3 -3
- package/dist/proxy.d.ts +16 -6
- package/dist/proxy.js +52 -16
- package/dist/runtime.d.ts +18 -12
- package/dist/runtime.js +18 -11
- package/dist/scaffold/add-channel.d.ts +14 -31
- package/dist/scaffold/add-channel.js +48 -112
- package/dist/scaffold/init.d.ts +9 -31
- package/dist/scaffold/init.js +45 -138
- package/dist/scaffold/templates/env.example +6 -4
- package/dist/scaffold/templates/fastagent.config.ts +36 -0
- package/dist/scaffold/templates.d.ts +3 -3
- package/dist/scaffold/templates.js +5 -8
- package/dist/scaffold/vendor-skill.d.ts +2 -5
- package/dist/scaffold/vendor-skill.js +19 -29
- package/dist/schedule/cron.d.ts +9 -5
- package/dist/schedule/cron.js +9 -11
- package/dist/schedule/discover.d.ts +12 -12
- package/dist/schedule/discover.js +43 -34
- package/dist/schedule/routine.d.ts +43 -0
- package/dist/schedule/routine.js +32 -0
- package/dist/schedule/run.d.ts +55 -0
- package/dist/schedule/run.js +146 -0
- package/dist/schedule/scheduler.d.ts +44 -55
- package/dist/schedule/scheduler.js +291 -265
- package/dist/schedule/state.d.ts +105 -10
- package/dist/schedule/state.js +293 -19
- package/dist/schedule/wake-alarm.d.ts +10 -23
- package/dist/schedule/wake-alarm.js +91 -121
- package/dist/schedule/wakeups.d.ts +23 -39
- package/dist/schedule/wakeups.js +33 -75
- package/dist/secrets-gate.d.ts +32 -0
- package/dist/secrets-gate.js +33 -0
- package/dist/service.d.ts +137 -98
- package/dist/service.js +250 -252
- package/dist/session-remote.d.ts +33 -44
- package/dist/session-remote.js +203 -192
- package/dist/session.d.ts +153 -228
- package/dist/session.js +33 -55
- package/dist/tunnel.d.ts +8 -36
- package/dist/tunnel.js +24 -65
- package/dist/version.d.ts +2 -2
- package/dist/version.js +2 -2
- package/package.json +10 -16
- package/dist/channels/agentcore-state.d.ts +0 -77
- package/dist/channels/agentcore-state.js +0 -276
- package/dist/channels/github/github.d.ts +0 -31
- package/dist/channels/github/github.js +0 -89
- package/dist/channels/github/scaffold/channel.ts +0 -25
- package/dist/cli/commands/attach.d.ts +0 -108
- package/dist/cli/commands/attach.js +0 -637
- package/dist/cli/commands/fire.d.ts +0 -7
- package/dist/cli/commands/fire.js +0 -38
- package/dist/cli/commands/schedule.d.ts +0 -12
- package/dist/cli/commands/schedule.js +0 -87
- package/dist/github.d.ts +0 -3
- package/dist/github.js +0 -2
- package/dist/scaffold/templates/fastagent.config.mjs +0 -23
- package/dist/schedule/audit.d.ts +0 -18
- package/dist/schedule/audit.js +0 -57
- package/dist/schedule/schedule.d.ts +0 -35
- package/dist/schedule/schedule.js +0 -23
|
@@ -1,107 +1,58 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `fastagent deploy agentcore` — the AWS Bedrock AgentCore deploy PLAN, computed from the resolved
|
|
3
|
-
* definition. Pure: facts in, artifact contents + an ordered runbook out; the CLI writes the files
|
|
4
|
-
* and prints the runbook. AgentCore is the fourth target, and its shape differs from Fly/Railway in
|
|
5
|
-
* kind, not degree:
|
|
6
|
-
*
|
|
7
|
-
* 1. **No public URL, no resident process.** The Runtime's only ingress is the SigV4
|
|
8
|
-
* `InvokeAgentRuntime` API, and compute is per-session microVMs that stop when idle. So the
|
|
9
|
-
* topology carries TWO extra pieces a Fly box never needs: a forwarder Lambda (public Function
|
|
10
|
-
* URL → envelope → InvokeAgentRuntime) fronting the webhooks, and EventBridge Scheduler rules
|
|
11
|
-
* delivering each cron slot (the container arms no resident timers — serve's externalClock mode).
|
|
12
|
-
* 2. **One template is the whole topology.** CloudFormation (`AWS::BedrockAgentCore::Runtime` is a
|
|
13
|
-
* first-class resource type) declares Runtime + roles + forwarder + schedules in one stack —
|
|
14
|
-
* unlike Railway, identity DOES live in a committed file; the stack name pins it.
|
|
15
|
-
* 3. **All ingress traffic shares ONE fixed runtime session** (`ingressSessionId`): fastagent's
|
|
16
|
-
* channel state is single-writer by design, and one session = at most one microVM at a time.
|
|
17
|
-
* AgentCore keeps a stopped session's id valid until the Runtime is deleted (a new compute is
|
|
18
|
-
* provisioned on the next invoke), so the fixed id needs no rotation. State lives on the
|
|
19
|
-
* platform's SessionStorage mount (`/mnt/state`) — persistent across compute stop/resume, no
|
|
20
|
-
* VPC/EFS required. Named trade-off: that state is tied to THIS Runtime resource — a stack
|
|
21
|
-
* replacement (renaming the runtime) starts blank. EFS (VPC mode) is the upgrade path when
|
|
22
|
-
* state must outlive the runtime; the runbook says so instead of silently shipping a VPC+NAT
|
|
23
|
-
* bill (~$35/mo) every deployment.
|
|
24
|
-
*
|
|
25
|
-
* The image is the SAME portable container every host ships (containerArtifacts) — AgentCore's
|
|
26
|
-
* extras (PORT=8080, FASTAGENT_AGENTCORE=1, the state dir) ride the Runtime resource's environment,
|
|
27
|
-
* never a forked Dockerfile. The build must be linux/arm64 (the platform requirement) — the ONE
|
|
28
|
-
* host where the build runs on the operator's machine (docker buildx) instead of remotely.
|
|
29
|
-
*/
|
|
1
|
+
/** `fastagent deploy agentcore` — the AWS Bedrock AgentCore deploy PLAN, computed from the resolved definition. */
|
|
30
2
|
import { createHash } from "node:crypto";
|
|
31
3
|
import { readFileSync } from "node:fs";
|
|
32
4
|
import { MAX_WEBHOOK_BODY_BYTES } from "../../channels/agentcore-limits.js";
|
|
33
5
|
import { SECRETS_DIRNAME } from "../../paths.js";
|
|
34
6
|
import { webhookKinds, webhookRunbook } from "../channel-ingress.js";
|
|
35
7
|
import { containerArtifacts } from "../container.js";
|
|
36
|
-
import {
|
|
37
|
-
function agentcoreTopology(input
|
|
38
|
-
|
|
39
|
-
translatedSchedules) {
|
|
40
|
-
const webhooks = input.channels.some((channel) => channel.ingress === "webhook");
|
|
41
|
-
return {
|
|
42
|
-
webhooks,
|
|
43
|
-
forwarder: webhooks || translatedSchedules > 0 || input.selfSchedule,
|
|
44
|
-
wakeAlarms: input.selfSchedule,
|
|
45
|
-
};
|
|
8
|
+
import { isEnvKey } from "../secrets.js";
|
|
9
|
+
function agentcoreTopology(input) {
|
|
10
|
+
return { webhooks: input.channels.some((channel) => channel.ingress === "webhook") };
|
|
46
11
|
}
|
|
47
|
-
/** SessionStorage mount
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
export const MOUNT = "/mnt/state";
|
|
51
|
-
/**
|
|
52
|
-
* FASTAGENT_SECRETS_DIR — the seeded-then-ROTATED auth.json, deliberately INSIDE the state root
|
|
53
|
-
* rather than beside it.
|
|
54
|
-
*
|
|
55
|
-
* Every other host mounts a real volume and puts the two machinery dirs side by side (`/data/.state`
|
|
56
|
-
* + `/data/.secrets`), because there the persistence boundary is the MOUNT POINT: anything under it
|
|
57
|
-
* survives. AgentCore has no volume. Its persistence boundary is `packStateRoot(stateRoot)` — the one
|
|
58
|
-
* directory tree the S3 snapshot copies out and back (channels/agentcore-state.ts) — while {@link MOUNT}
|
|
59
|
-
* itself is wiped on every runtime version update, i.e. on every deploy.
|
|
60
|
-
*
|
|
61
|
-
* So the sibling layout would put credentials INSIDE the mount but OUTSIDE the snapshot: nothing
|
|
62
|
-
* copies them out, the platform wipes them, and the next microVM re-seeds the deploy-time copy. With
|
|
63
|
-
* single-use OAuth refresh tokens that is a slow-motion outage — the box works until the seeded token
|
|
64
|
-
* is rotated away, then loses model access with only a redeploy to restore it.
|
|
65
|
-
*
|
|
66
|
-
* Nesting is what makes agentcore-state.ts's stated contract ("restores VERBATIM — including
|
|
67
|
-
* auth.json") reachable at all; `packStateRoot` walks the whole tree, so no snapshot code knows about
|
|
68
|
-
* this. Tests assert the containment, not just the two names — the sibling spelling looks tidier and
|
|
69
|
-
* reintroduces the outage silently.
|
|
70
|
-
*/
|
|
12
|
+
/** The platform's managed SessionStorage mount (AgentCore requires exactly `/mnt/<one-level>`). */
|
|
13
|
+
export const MOUNT = "/mnt/data";
|
|
14
|
+
/** Beside the state root on the one mount, as every volume-backed host does. */
|
|
71
15
|
export const SECRETS_DIR = `${MOUNT}/${SECRETS_DIRNAME}`;
|
|
72
16
|
/**
|
|
73
|
-
* How long an idle session keeps its microVM
|
|
74
|
-
*
|
|
75
|
-
* activity, while CPU stops billing the moment the agent stops working. 3 minutes rather than the
|
|
76
|
-
* platform's 15: the tail shrinks 5×, and the cost is a cold start (image + Node + snapshot restore)
|
|
77
|
-
* for anyone who returns after a longer gap. `/ping` reports HealthyBusy + time_of_last_update while
|
|
78
|
-
* work is in flight (the FIELD is what the platform's idle measurement actually reads — agentcore.ts),
|
|
79
|
-
* so this timer only ever starts once the agent has genuinely settled — a long turn is never cut short.
|
|
80
|
-
* AWS accepts 60–28800.
|
|
17
|
+
* How long an idle session keeps its microVM when the config names no other value. Below AWS's own default (900) on
|
|
18
|
+
* purpose: memory bills for the whole idle tail, so the cheap posture is the default and a longer one is a choice.
|
|
81
19
|
*/
|
|
82
|
-
export const
|
|
83
|
-
/**
|
|
84
|
-
* simply gets fresh compute with the same storage. */
|
|
20
|
+
export const DEFAULT_IDLE_TIMEOUT_SECONDS = 180;
|
|
21
|
+
/** One session's compute lifetime — AWS's own default. Both lifecycle fields cap at 1209600 (14 d). */
|
|
85
22
|
export const MAX_LIFETIME_SECONDS = 28800;
|
|
86
|
-
/** The state snapshot's object key in the deployment bucket (one object; see agentcore-state.ts). */
|
|
87
|
-
export const STATE_KEY = "state/snapshot.json.gz";
|
|
88
|
-
/** The forwarder artifact. Named `index.js` because it IS the Lambda deployment package's entry:
|
|
89
|
-
* zipping it as-is produces a valid package (`Handler: index.handler`), with nothing to rename. */
|
|
90
23
|
export const FORWARDER_FILE = "lambda/index.js";
|
|
91
|
-
/**
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
|
|
24
|
+
/**
|
|
25
|
+
* Where the forwarder puts the one-shot schedule for a wake-up. The id inside the name is minted in the
|
|
26
|
+
* container, so this prefix is the only handle anything outside has on the alarms a deployment has pending.
|
|
27
|
+
*/
|
|
28
|
+
export function wakeAlarmPrefix(name) {
|
|
29
|
+
return `fa-${name}-wk-`;
|
|
30
|
+
}
|
|
31
|
+
/** Account-suffixed artifact bucket for S3's global namespace. */
|
|
32
|
+
export function deploymentBucketName(name, account) {
|
|
95
33
|
return `fa-${name}-${account}`;
|
|
96
34
|
}
|
|
97
|
-
/**
|
|
98
|
-
*
|
|
99
|
-
|
|
100
|
-
|
|
35
|
+
/**
|
|
36
|
+
* AgentCore env values max 2048 chars — a real OAuth auth.json's base64 exceeds it, so each carrier is CHUNKED across
|
|
37
|
+
* `<NAME>` + `_2`… (collectChunked reassembles at boot): FASTAGENT_AUTH_SEED for the credential, FASTAGENT_ENV for the
|
|
38
|
+
* value file's variables.
|
|
39
|
+
*/
|
|
40
|
+
export const CARRIER_CHUNK_SIZE = 2000;
|
|
41
|
+
export const CARRIER_MAX_CHUNKS = 4;
|
|
42
|
+
/** The template's two chunked carriers: CloudFormation parameter prefix → container env-var name. */
|
|
43
|
+
export const CARRIERS = [
|
|
44
|
+
{ param: "FastagentAuthSeed", env: "FASTAGENT_AUTH_SEED", what: "base64 auth.json" },
|
|
45
|
+
{ param: "FastagentEnv", env: "FASTAGENT_ENV", what: "base64 JSON of the value file's variables" },
|
|
46
|
+
];
|
|
47
|
+
/** Chunk `i` (0-based) of a carrier: its parameter and env-var names. */
|
|
48
|
+
export function carrierChunk(carrier, i) {
|
|
49
|
+
return i === 0
|
|
50
|
+
? { param: carrier.param, env: carrier.env }
|
|
51
|
+
: { param: `${carrier.param}${i + 1}`, env: `${carrier.env}_${i + 1}` };
|
|
52
|
+
}
|
|
101
53
|
/** The generated template's filename (namespaced under the kit in the agentDir layout). */
|
|
102
54
|
export const TEMPLATE_FILE = "agentcore.template.yaml";
|
|
103
|
-
/** The generated template's first-line marker
|
|
104
|
-
* "did fastagent generate this?" check (deploy's drift gate), so they cannot drift apart. */
|
|
55
|
+
/** The generated template's first-line marker. */
|
|
105
56
|
export const GENERATED_TEMPLATE_MARKER = "# Generated by `fastagent deploy agentcore`";
|
|
106
57
|
/** Whether an on-disk template is fastagent-generated (vs hand-written — kept, never gated). */
|
|
107
58
|
export function isGeneratedAgentcoreTemplate(content) {
|
|
@@ -119,28 +70,48 @@ export function toRuntimeName(basename) {
|
|
|
119
70
|
const slug = basename.replace(/[^a-zA-Z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
120
71
|
return (/^[a-zA-Z]/.test(slug) ? slug : `agent_${slug || "fastagent"}`).slice(0, 48);
|
|
121
72
|
}
|
|
122
|
-
/** The
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
return `fastagent-ingress-${name}`.padEnd(33, "0").slice(0, 128);
|
|
73
|
+
/** The forwarder Lambda's name — and therefore its log group, which AWS derives from it. */
|
|
74
|
+
function forwarderFunctionName(name) {
|
|
75
|
+
return `${agentcoreStackName(name)}-forwarder`;
|
|
126
76
|
}
|
|
127
|
-
/**
|
|
128
|
-
*
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
77
|
+
/**
|
|
78
|
+
* The two names every part of this host derives from the deployment name, and the reason they are functions:
|
|
79
|
+
* a deploy CREATES them and a destroy LOOKS FOR them, so a drift between the two spellings is silent in the
|
|
80
|
+
* worst direction. `describe-repositories` on a mis-spelled repo answers `RepositoryNotFoundException`, which
|
|
81
|
+
* is "already gone" — the teardown would report nothing left to delete while the repository kept billing.
|
|
82
|
+
*/
|
|
83
|
+
export function agentcoreStackName(name) {
|
|
84
|
+
return `fastagent-${name}`;
|
|
85
|
+
}
|
|
86
|
+
export function agentcoreRepoName(name) {
|
|
87
|
+
return `fastagent/${name}`;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Where a RUNTIME's logs land, by the identifier the caller happens to hold: `fastagent logs` knows the runtime id
|
|
91
|
+
* from the stack's `RuntimeArn`, `destroy` knows only the {@link toRuntimeName} the template deployed — and AgentCore
|
|
92
|
+
* names the id `<AgentRuntimeName>-<suffix>`, so the same prefix serves both. MEASURED (ap-southeast-1, 2026-09-22):
|
|
93
|
+
* a deploy of `destroy-probe-yehg` produced `/aws/bedrock-agentcore/runtimes/destroy_probe_yehg-6oR1zq7pBZ-DEFAULT`.
|
|
94
|
+
*
|
|
95
|
+
* ONE OWNER because of what the callers do with it: `logs` picks a group to tail, `destroy` DELETES everything the
|
|
96
|
+
* prefix matches. The trailing `-` is what keeps `probe-` off `probe2-…`, and a second hand-spelled copy of this
|
|
97
|
+
* string is a copy that can lose it.
|
|
98
|
+
*/
|
|
99
|
+
export function runtimeLogGroupPrefix(runtimeOrName) {
|
|
100
|
+
return `/aws/bedrock-agentcore/runtimes/${runtimeOrName}-`;
|
|
135
101
|
}
|
|
136
102
|
/**
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
* (SUN..SAT) pass through. A numeric range whose endpoints INVERT under renumbering (`5-7` → `6-1`)
|
|
141
|
-
* wraps across the week — not expressible as an EventBridge range — and is refused, never silently
|
|
142
|
-
* reordered.
|
|
103
|
+
* Where the forwarder's logs land. Lambda creates this group itself, from the function name, so three parties have
|
|
104
|
+
* to agree on a string none of them can check against the others: the template that names the function, the runbook
|
|
105
|
+
* and `--run` summary that tell an operator to set its retention, and `fastagent logs --source forwarder`.
|
|
143
106
|
*/
|
|
107
|
+
export function forwarderLogGroup(name) {
|
|
108
|
+
return `/aws/lambda/${forwarderFunctionName(name)}`;
|
|
109
|
+
}
|
|
110
|
+
/** The ONE fixed ingress session id (webhooks + routine fires). */
|
|
111
|
+
export function ingressSessionId(name) {
|
|
112
|
+
return `fastagent-ingress-${name}`.padEnd(33, "0").slice(0, 128);
|
|
113
|
+
}
|
|
114
|
+
/** Remap ONE day-of-week field from standard cron numbering (0–7, 0/7 = Sunday) to EventBridge's (1–7, 1 = Sunday). */
|
|
144
115
|
function mapDowField(dow) {
|
|
145
116
|
const items = [];
|
|
146
117
|
for (const item of dow.split(",")) {
|
|
@@ -175,16 +146,7 @@ function mapDowField(dow) {
|
|
|
175
146
|
}
|
|
176
147
|
return { value: items.join(",") };
|
|
177
148
|
}
|
|
178
|
-
/**
|
|
179
|
-
* Translate a 5-field cron into EventBridge Scheduler's `cron(m h dom mon dow *)`, or say why it
|
|
180
|
-
* can't be. The two dialects disagree exactly where silent translation would misfire:
|
|
181
|
-
* - EventBridge numbers day-of-week 1–7 (1 = Sunday); standard cron uses 0–6 (0/7 = Sunday) —
|
|
182
|
-
* numeric dow values and range endpoints are remapped ({@link mapDowField}); steps and names
|
|
183
|
-
* pass through; a range that wraps under renumbering is refused.
|
|
184
|
-
* - EventBridge requires `?` in dom or dow: a `*` on either side becomes `?`; BOTH restricted is
|
|
185
|
-
* standard cron's OR semantics, which EventBridge cannot express — refused, never approximated.
|
|
186
|
-
* - A 6-field (seconds) expression and L/# day-of-week forms are refused for the same reason.
|
|
187
|
-
*/
|
|
149
|
+
/** Translate a 5-field cron into EventBridge Scheduler's `cron(m h dom mon dow *)`, or say why it can't be. */
|
|
188
150
|
export function toEventBridgeCron(cron) {
|
|
189
151
|
const fields = cron.trim().split(/\s+/);
|
|
190
152
|
if (fields.length !== 5) {
|
|
@@ -194,13 +156,7 @@ export function toEventBridgeCron(cron) {
|
|
|
194
156
|
if (/[L#]/i.test(dow) || /[L#]/i.test(dom)) {
|
|
195
157
|
return { error: "L/# day forms don't translate to EventBridge numbering — set this schedule up manually" };
|
|
196
158
|
}
|
|
197
|
-
// `?` FIRST, and not as a synonym for `*`.
|
|
198
|
-
// and — unlike `*` — does NOT trigger cron's "one field is unrestricted, so the other governs"
|
|
199
|
-
// special case. It therefore ORs with the other field to mean EVERY DAY, whatever that field says.
|
|
200
|
-
// Measured against croner: `0 9 ? * MON` and `0 9 1 * ?` both fire daily, while `0 9 * * MON`
|
|
201
|
-
// fires on Mondays. Translating `?` to `*` would deploy a rule that fires on a DIFFERENT set of
|
|
202
|
-
// days than the same file fires on locally — the silent divergence this whole target exists to
|
|
203
|
-
// avoid — so a `?` in either field becomes an explicitly daily EventBridge rule.
|
|
159
|
+
// `?` FIRST, and not as a synonym for `*`.
|
|
204
160
|
if (dom === "?" || dow === "?") {
|
|
205
161
|
return { expression: `cron(${min} ${hour} * ${mon} ? *)` };
|
|
206
162
|
}
|
|
@@ -222,36 +178,18 @@ function logicalId(name) {
|
|
|
222
178
|
const slug = name.replace(/[^a-zA-Z0-9]+/g, "");
|
|
223
179
|
return slug.charAt(0).toUpperCase() + slug.slice(1) || "Schedule";
|
|
224
180
|
}
|
|
225
|
-
/** YAML single-quoted scalar (the one escape: `'` doubles).
|
|
181
|
+
/** YAML single-quoted scalar (the one escape: `'` doubles). */
|
|
226
182
|
function yamlSingleQuote(value) {
|
|
227
183
|
return `'${value.replace(/'/g, "''")}'`;
|
|
228
184
|
}
|
|
229
185
|
/**
|
|
230
|
-
* The forwarder Lambda source — `forwarder.js` beside this file, the ONE text both the deployment
|
|
231
|
-
*
|
|
232
|
-
* literal: it is linted, and it needs no interpolation — the one value it once took from here
|
|
233
|
-
* (`MAX_WEBHOOK_BODY_BYTES`) rides in as a Lambda environment variable. Zero-dependency: the
|
|
234
|
-
* Lambda Node runtime bundles AWS SDK v3. CommonJS ON PURPOSE: the package's entry lands as
|
|
235
|
-
* `index.js` ({@link zipSingleFile}), where ESM `import` is a syntax error. Two
|
|
236
|
-
* event shapes: a Function URL webhook (reconstructed verbatim into a `webhook` envelope; the
|
|
237
|
-
* channel's REAL response rides back inside the transport reply and is re-emitted byte-exact —
|
|
238
|
-
* Feishu's URL-verification challenge depends on it), and an EventBridge Scheduler fire
|
|
239
|
-
* (`{ scheduleFire }`, slot = the scheduled instant — the container's idempotency key).
|
|
240
|
-
*
|
|
241
|
-
* It ships as an S3 object rather than inline `ZipFile`, so there is no 4096-byte ceiling on it — a
|
|
242
|
-
* constraint this comment asserted for long after the template stopped inlining, which is the kind
|
|
243
|
-
* of stale rule that stops the next person making a correct change.
|
|
186
|
+
* The forwarder Lambda source — `forwarder.js` beside this file, the ONE text both the deployment package and the
|
|
187
|
+
* readable `lambda/index.js` artifact are generated from.
|
|
244
188
|
*/
|
|
245
189
|
export function forwarderSource() {
|
|
246
190
|
return readFileSync(new URL("./forwarder.js", import.meta.url), "utf8");
|
|
247
191
|
}
|
|
248
|
-
/**
|
|
249
|
-
* The EventBridge physical name for a schedule. A schedule's local name is an arbitrary MODULE FILE
|
|
250
|
-
* NAME (`schedules/晨报.ts`, `schedules/deploy check.ts`), while AWS requires `[0-9A-Za-z-_.]+` within
|
|
251
|
-
* 64 chars — and the `fa-<agent>-` prefix already eats up to 44 of them. So: sanitize, bound the
|
|
252
|
-
* readable part, and end with a hash of the ORIGINAL name, which keeps distinct schedules distinct
|
|
253
|
-
* where sanitizing or truncation would have merged them (one rule silently firing for two).
|
|
254
|
-
*/
|
|
192
|
+
/** The EventBridge physical name for a schedule. */
|
|
255
193
|
export function scheduleResourceName(agent, schedule) {
|
|
256
194
|
const prefix = `fa-${agent}-`;
|
|
257
195
|
const hash = createHash("sha256").update(schedule).digest("hex").slice(0, 8);
|
|
@@ -262,61 +200,38 @@ export function scheduleResourceName(agent, schedule) {
|
|
|
262
200
|
/** The CloudFormation template — the whole topology in one stack. */
|
|
263
201
|
function template(input, translated, topology) {
|
|
264
202
|
const runtimeName = toRuntimeName(input.name);
|
|
265
|
-
const
|
|
266
|
-
const secrets = deploymentSecrets(input.modelAuth, input.channels, input.extraSecrets);
|
|
203
|
+
const idleTimeout = input.idleTimeoutSeconds ?? DEFAULT_IDLE_TIMEOUT_SECONDS;
|
|
267
204
|
const forwarderFnArn = `!Sub arn:aws:lambda:\${AWS::Region}:\${AWS::AccountId}:function:fastagent-${input.name}-forwarder`;
|
|
268
|
-
// Secret env vars ride CFN NoEcho parameters.
|
|
269
|
-
// so a `--run` OAuth carry has a slot; required secrets have NO default — `cloudformation deploy`
|
|
270
|
-
// fails loudly without a value instead of booting a half-configured box.
|
|
205
|
+
// Secret env vars ride CFN NoEcho parameters.
|
|
271
206
|
const params = [
|
|
272
207
|
` ImageUri:`,
|
|
273
208
|
` Type: String`,
|
|
274
209
|
` Description: ECR image URI (linux/arm64) — <account>.dkr.ecr.<region>.amazonaws.com/<repo>:<tag>`,
|
|
275
210
|
];
|
|
276
|
-
|
|
277
|
-
params.push(` StateBucket:`, ` Type: String`, ` Description: S3 bucket holding the forwarder deployment package + the agent's state snapshot (created outside this stack)`, ` ForwarderS3Key:`, ` Type: String`, ` Description: key of the forwarder .zip in StateBucket — CONTENT-HASHED, so new code is a new value and CloudFormation rolls the function`);
|
|
278
|
-
}
|
|
211
|
+
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`);
|
|
279
212
|
const envLines = [
|
|
280
213
|
` PORT: "8080"`, // the Runtime service contract's fixed port (config.http.port does not apply here)
|
|
281
214
|
` FASTAGENT_AGENTCORE: "1"`, // serve mounts /invocations + /ping, arms no resident cron
|
|
282
|
-
`
|
|
283
|
-
|
|
284
|
-
// exactly one tree. See {@link SECRETS_DIR}: the sibling layout every other host uses would leave a
|
|
285
|
-
// rotated OAuth credential outside it, i.e. discarded with the microVM.
|
|
215
|
+
` FASTAGENT_STORAGE_DIR: ${MOUNT}`,
|
|
216
|
+
` FASTAGENT_STATE_DIR: ${MOUNT}/.state`,
|
|
286
217
|
` FASTAGENT_SECRETS_DIR: ${SECRETS_DIR}`,
|
|
287
218
|
];
|
|
288
|
-
//
|
|
289
|
-
//
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
const p = cfnParamName(s.name);
|
|
298
|
-
params.push(` ${p}:`, ` Type: String`);
|
|
299
|
-
if (!s.required)
|
|
300
|
-
params.push(` Default: ""`);
|
|
301
|
-
params.push(` NoEcho: true`, ` Description: ${s.hint}`);
|
|
302
|
-
envLines.push(` ${s.name}: !Ref ${p}`);
|
|
303
|
-
}
|
|
304
|
-
if (needsForwarder) {
|
|
305
|
-
// The INGRESS secret authenticates forwarder→runtime envelopes. Without it the envelope union is
|
|
306
|
-
// an unauthenticated control plane: `InvokeAgentRuntime` is an ordinary IAM action, so any
|
|
307
|
-
// principal holding it could forge a `schedule-fire`, or ride `state`/`wake` on a public `invoke`
|
|
308
|
-
// to redirect the state snapshot (exfiltrating auth.json) or the wake-alarm callback (leaking the
|
|
309
|
-
// wake secret) to an address of their choosing. Public `invoke` stays unauthenticated by design —
|
|
310
|
-
// it is the programmatic data plane — but it may not carry internal fields.
|
|
311
|
-
params.push(` FastagentIngressSecret:`, ` Type: String`, ` NoEcho: true`, ` Description: shared secret authenticating forwarder→runtime envelopes (any random string; --run mints one)`);
|
|
312
|
-
envLines.push(` FASTAGENT_INGRESS_SECRET: !Ref FastagentIngressSecret`);
|
|
313
|
-
}
|
|
314
|
-
if (input.selfSchedule) {
|
|
315
|
-
// The wake-alarm shared secret: the container authenticates its alarm callbacks to the forwarder
|
|
316
|
-
// with it. Required (no default) — a selfSchedule deployment without it would silently degrade.
|
|
317
|
-
params.push(` FastagentWakeSecret:`, ` Type: String`, ` NoEcho: true`, ` Description: shared secret between the container and the forwarder's wake-alarm callback (any random string; --run mints one)`);
|
|
318
|
-
envLines.push(` FASTAGENT_WAKE_SECRET: !Ref FastagentWakeSecret`);
|
|
219
|
+
// Both carriers are chunked (env values max 2048 chars — see CARRIER_CHUNK_SIZE): N parameters, each riding its
|
|
220
|
+
// own env var; `start` reassembles them. ONE parameter set for every variable, so the template does not change
|
|
221
|
+
// when the value file gains a name.
|
|
222
|
+
for (const carrier of CARRIERS) {
|
|
223
|
+
for (let i = 0; i < CARRIER_MAX_CHUNKS; i++) {
|
|
224
|
+
const { param, env } = carrierChunk(carrier, i);
|
|
225
|
+
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`);
|
|
226
|
+
envLines.push(` ${env}: !Ref ${param}`);
|
|
227
|
+
}
|
|
319
228
|
}
|
|
229
|
+
// IAM invocation permission does not prove forwarder origin. Internal envelopes need a secret.
|
|
230
|
+
params.push(` FastagentIngressSecret:`, ` Type: String`, ` NoEcho: true`, ` Description: shared secret authenticating forwarder→runtime envelopes (any random string; --run mints one)`);
|
|
231
|
+
envLines.push(` FASTAGENT_INGRESS_SECRET: !Ref FastagentIngressSecret`);
|
|
232
|
+
// The wake-alarm shared secret: the container authenticates its alarm callbacks to the forwarder with it.
|
|
233
|
+
params.push(` FastagentWakeSecret:`, ` Type: String`, ` NoEcho: true`, ` Description: shared secret between the container and the forwarder's wake-alarm callback (any random string; --run mints one)`);
|
|
234
|
+
envLines.push(` FASTAGENT_WAKE_SECRET: !Ref FastagentWakeSecret`);
|
|
320
235
|
const lines = [
|
|
321
236
|
`${GENERATED_TEMPLATE_MARKER}. Edit freely — deploy then treats it as hand-written and never gates on drift.`,
|
|
322
237
|
`AWSTemplateFormatVersion: "2010-09-09"`,
|
|
@@ -368,72 +283,43 @@ function template(input, translated, topology) {
|
|
|
368
283
|
` RoleArn: !GetAtt ExecutionRole.Arn`,
|
|
369
284
|
` ProtocolConfiguration: HTTP`,
|
|
370
285
|
` NetworkConfiguration: { NetworkMode: PUBLIC }`,
|
|
371
|
-
` # SessionStorage
|
|
372
|
-
` #
|
|
373
|
-
` #
|
|
374
|
-
` #
|
|
375
|
-
` # no AWS credentials). A persistent MOUNT instead needs EfsAccessPoint + VPC mode, which`,
|
|
376
|
-
` # forces a NAT gateway for model/channel egress (~$33/mo) — deliberately not the default.`,
|
|
286
|
+
` # Managed SessionStorage: no VPC, no NAT, no operator-owned filesystem. It survives compute`,
|
|
287
|
+
` # stop/resume, and AWS RESETS it on every runtime version update (= every deploy) and after`,
|
|
288
|
+
` # 14 idle days — so a deploy replaces the state along with the image. Cross-deploy memory`,
|
|
289
|
+
` # would need EFS or S3 Files, both VPC-only and therefore a standing NAT bill.`,
|
|
377
290
|
` FilesystemConfigurations:`,
|
|
378
291
|
` - SessionStorage: { MountPath: ${MOUNT} }`,
|
|
379
|
-
` # Idle ${
|
|
380
|
-
` # lifetime ${MAX_LIFETIME_SECONDS}s
|
|
292
|
+
` # Idle ${idleTimeout}s (the ping's HealthyBusy + time_of_last_update keeps BUSY sessions alive), max compute`,
|
|
293
|
+
` # lifetime ${MAX_LIFETIME_SECONDS}s; the session id stays valid, so the next invoke`,
|
|
381
294
|
` # just gets fresh compute with the same storage. Memory bills per second for the whole`,
|
|
382
|
-
` # session INCLUDING the idle tail, so a shorter tail is the main cost lever here
|
|
383
|
-
`
|
|
295
|
+
` # session INCLUDING the idle tail, so a shorter tail is the main cost lever here —`,
|
|
296
|
+
` # deploy.agentcore.idleTimeoutSeconds buys latency back for a chat workload.`,
|
|
297
|
+
` LifecycleConfiguration: { IdleRuntimeSessionTimeout: ${idleTimeout}, MaxLifetime: ${MAX_LIFETIME_SECONDS} }`,
|
|
384
298
|
` EnvironmentVariables:`,
|
|
385
299
|
...envLines,
|
|
386
300
|
];
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
` - Effect: Allow # wake alarms: mirror pending wake-ups into one-shot schedules`,
|
|
391
|
-
` Action: [scheduler:CreateSchedule, scheduler:UpdateSchedule]`,
|
|
392
|
-
` Resource: !Sub arn:aws:scheduler:\${AWS::Region}:\${AWS::AccountId}:schedule/default/fa-${input.name}-wk-*`,
|
|
393
|
-
` - Effect: Allow # hand the poke schedules their invoke role`,
|
|
394
|
-
` Action: iam:PassRole`,
|
|
395
|
-
` Resource: !GetAtt WakeSchedulerRole.Arn`,
|
|
396
|
-
]
|
|
397
|
-
: []), ...(needsForwarder
|
|
398
|
-
? [
|
|
399
|
-
` - Effect: Allow # self-resolve callback URL for state-URL refresh / wake alarms`,
|
|
400
|
-
` Action: lambda:GetFunctionUrlConfig`,
|
|
401
|
-
` Resource: ${forwarderFnArn}`,
|
|
402
|
-
]
|
|
403
|
-
: []), ``, ` Forwarder:`, ` Type: AWS::Lambda::Function`, ` Properties:`, ` FunctionName: fastagent-${input.name}-forwarder`, ` Runtime: nodejs22.x`, ` Handler: index.handler`, ` # Webhook ACKs are fast, but schedule-fire holds the connection for the WHOLE agent turn`, ` # (claim-before-run means a timeout never double-fires; the turn also continues and is`, ` # audited container-side). EventBridge→Lambda is async, so the long timeout costs nothing.`, ` Timeout: 900`, ` MemorySize: 256`, ` Role: !GetAtt ForwarderRole.Arn`, ` Environment:`, ` Variables:`, ` RUNTIME_ARN: !GetAtt Runtime.AgentRuntimeArn`, ` INGRESS_SESSION_ID: ${ingressSessionId(input.name)}`, ...(needsForwarder ? [` STATE_REFRESH_SECRET: !Ref FastagentIngressSecret`] : []), ...(topology.webhooks ? [` WEBHOOKS_ENABLED: "1"`] : []), ...(input.selfSchedule
|
|
404
|
-
? [
|
|
405
|
-
` WAKE_SECRET: !Ref FastagentWakeSecret`,
|
|
406
|
-
` WAKE_ROLE_ARN: !GetAtt WakeSchedulerRole.Arn`,
|
|
407
|
-
` WAKE_PREFIX: fa-${input.name}-wk-`,
|
|
408
|
-
]
|
|
409
|
-
: []), ` INGRESS_SECRET: !Ref FastagentIngressSecret`, ` STATE_BUCKET: !Ref StateBucket`, ` STATE_KEY: ${STATE_KEY}`, ` MAX_WEBHOOK_BODY_BYTES: "${MAX_WEBHOOK_BODY_BYTES}"`, ` # From S3, not inline: the forwarder mints SigV4-presigned URLs for the state snapshot and`, ` # no longer fits CloudFormation's 4096-byte inline cap. The key is content-hashed, so a`, ` # code change is a parameter change — CloudFormation cannot miss it.`, ` Code:`, ` S3Bucket: !Ref StateBucket`, ` S3Key: !Ref ForwarderS3Key`);
|
|
410
|
-
}
|
|
411
|
-
if (needsForwarder) {
|
|
412
|
-
lines.push(``, ` ForwarderUrl:`, ` Type: AWS::Lambda::Url`, ` Properties:`, ` TargetFunctionArn: !GetAtt Forwarder.Arn`, ` # NONE is deliberate: webhook callers (Telegram/Feishu) cannot SigV4-sign. Authenticity is`, ` # verified downstream by each channel (secret token / signature), exactly as on every host.`, ` AuthType: NONE`, ``, ` ForwarderUrlPermission:`, ` Type: AWS::Lambda::Permission`, ` Properties:`, ` FunctionName: !Ref Forwarder`, ` Action: lambda:InvokeFunctionUrl`, ` Principal: "*"`, ` FunctionUrlAuthType: NONE`, ``, ` # Function URLs created after Oct 2025 require lambda:InvokeFunction IN ADDITION to`, ` # lambda:InvokeFunctionUrl for public (NONE) access — with only the first, every request 403s`, ` # (found by the first real deploy). InvokedViaFunctionUrl scopes it to URL traffic: without it`, ` # the bare * principal would also let any AWS principal call the Lambda API directly, bypassing`, ` # the Function URL event shape to forge internal events.`, ` ForwarderInvokePermission:`, ` Type: AWS::Lambda::Permission`, ` Properties:`, ` FunctionName: !Ref Forwarder`, ` Action: lambda:InvokeFunction`, ` Principal: "*"`, ` InvokedViaFunctionUrl: true`);
|
|
413
|
-
if (input.selfSchedule) {
|
|
414
|
-
lines.push(``, ` # The role the wake-poke schedules assume to invoke the forwarder. Its policy names the`, ` # function by CONSTRUCTED arn (not !Ref) — the forwarder's env references this role, so a`, ` # !Ref back would be a circular dependency.`, ` WakeSchedulerRole:`, ` Type: AWS::IAM::Role`, ` Properties:`, ` AssumeRolePolicyDocument:`, ` Version: "2012-10-17"`, ` Statement:`, ` - Effect: Allow`, ` Principal: { Service: scheduler.amazonaws.com }`, ` Action: sts:AssumeRole`, ` Condition:`, ` StringEquals: { aws:SourceAccount: !Ref AWS::AccountId }`, ` Policies:`, ` - PolicyName: poke-forwarder`, ` PolicyDocument:`, ` Version: "2012-10-17"`, ` Statement:`, ` - Effect: Allow`, ` Action: lambda:InvokeFunction`, ` Resource: ${forwarderFnArn}`);
|
|
415
|
-
}
|
|
416
|
-
}
|
|
301
|
+
lines.push(``, ` ForwarderRole:`, ` Type: AWS::IAM::Role`, ` Properties:`, ` AssumeRolePolicyDocument:`, ` Version: "2012-10-17"`, ` Statement:`, ` - Effect: Allow`, ` Principal: { Service: lambda.amazonaws.com }`, ` Action: sts:AssumeRole`, ` ManagedPolicyArns: [arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole]`, ` Policies:`, ` - PolicyName: invoke-runtime`, ` PolicyDocument:`, ` Version: "2012-10-17"`, ` Statement:`, ` - Effect: Allow`, ` Action: bedrock-agentcore:InvokeAgentRuntime`, ` Resource:`, ` - !GetAtt Runtime.AgentRuntimeArn`, ` - !Sub "\${Runtime.AgentRuntimeArn}/*"`, ` - Effect: Allow # wake alarms: mirror pending wake-ups into one-shot schedules`, ` Action: [scheduler:CreateSchedule, scheduler:UpdateSchedule]`, ` Resource: !Sub arn:aws:scheduler:\${AWS::Region}:\${AWS::AccountId}:schedule/default/${wakeAlarmPrefix(input.name)}*`, ` - Effect: Allow # hand the poke schedules their invoke role`, ` Action: iam:PassRole`, ` Resource: !GetAtt WakeSchedulerRole.Arn`, ` - Effect: Allow # self-resolve callback URL for state-URL refresh / wake alarms`, ` Action: lambda:GetFunctionUrlConfig`, ` Resource: ${forwarderFnArn}`, ``, ` Forwarder:`, ` Type: AWS::Lambda::Function`, ` Properties:`, ` FunctionName: ${forwarderFunctionName(input.name)}`, ` Runtime: nodejs22.x`, ` Handler: index.handler`, ` # Webhook ACKs are fast, but routine-fire holds the connection for the WHOLE agent turn`, ` # (claim-before-run means a timeout never double-fires; the turn also continues and is`, ` # audited container-side). EventBridge→Lambda is async, so the long timeout costs nothing.`, ` Timeout: 900`, ` MemorySize: 256`, ` Role: !GetAtt ForwarderRole.Arn`, ` Environment:`, ` Variables:`, ` RUNTIME_ARN: !GetAtt Runtime.AgentRuntimeArn`, ` INGRESS_SESSION_ID: ${ingressSessionId(input.name)}`, ...(topology.webhooks ? [` WEBHOOKS_ENABLED: "1"`] : []), ` WAKE_SECRET: !Ref FastagentWakeSecret`, ` WAKE_ROLE_ARN: !GetAtt WakeSchedulerRole.Arn`, ` WAKE_PREFIX: ${wakeAlarmPrefix(input.name)}`, ` INGRESS_SECRET: !Ref FastagentIngressSecret`, ` MAX_WEBHOOK_BODY_BYTES: "${MAX_WEBHOOK_BODY_BYTES}"`, ` # A content-hashed key makes code changes visible to CloudFormation.`, ` Code:`, ` S3Bucket: !Ref ForwarderBucket`, ` S3Key: !Ref ForwarderS3Key`);
|
|
302
|
+
lines.push(``, ` ForwarderUrl:`, ` Type: AWS::Lambda::Url`, ` Properties:`, ` TargetFunctionArn: !GetAtt Forwarder.Arn`, ` # NONE is deliberate: webhook callers (Telegram/Feishu) cannot SigV4-sign. Authenticity is`, ` # verified downstream by each channel (secret token / signature), exactly as on every host.`, ` AuthType: NONE`, ``, ` ForwarderUrlPermission:`, ` Type: AWS::Lambda::Permission`, ` Properties:`, ` FunctionName: !Ref Forwarder`, ` Action: lambda:InvokeFunctionUrl`, ` Principal: "*"`, ` FunctionUrlAuthType: NONE`, ``, ` # Function URLs created after Oct 2025 require lambda:InvokeFunction IN ADDITION to`, ` # lambda:InvokeFunctionUrl for public (NONE) access — with only the first, every request 403s`, ` # (found by the first real deploy). InvokedViaFunctionUrl scopes it to URL traffic: without it`, ` # the bare * principal would also let any AWS principal call the Lambda API directly, bypassing`, ` # the Function URL event shape to forge internal events.`, ` ForwarderInvokePermission:`, ` Type: AWS::Lambda::Permission`, ` Properties:`, ` FunctionName: !Ref Forwarder`, ` Action: lambda:InvokeFunction`, ` Principal: "*"`, ` InvokedViaFunctionUrl: true`);
|
|
303
|
+
lines.push(``, ` # The role the wake-poke schedules assume to invoke the forwarder. Its policy names the`, ` # function by CONSTRUCTED arn (not !Ref) — the forwarder's env references this role, so a`, ` # !Ref back would be a circular dependency.`, ` WakeSchedulerRole:`, ` Type: AWS::IAM::Role`, ` Properties:`, ` AssumeRolePolicyDocument:`, ` Version: "2012-10-17"`, ` Statement:`, ` - Effect: Allow`, ` Principal: { Service: scheduler.amazonaws.com }`, ` Action: sts:AssumeRole`, ` Condition:`, ` StringEquals: { aws:SourceAccount: !Ref AWS::AccountId }`, ` Policies:`, ` - PolicyName: poke-forwarder`, ` PolicyDocument:`, ` Version: "2012-10-17"`, ` Statement:`, ` - Effect: Allow`, ` Action: lambda:InvokeFunction`, ` Resource: ${forwarderFnArn}`);
|
|
417
304
|
if (translated.length > 0) {
|
|
418
305
|
lines.push(``, ` SchedulerRole:`, ` Type: AWS::IAM::Role`, ` Properties:`, ` AssumeRolePolicyDocument:`, ` Version: "2012-10-17"`, ` Statement:`, ` - Effect: Allow`, ` Principal: { Service: scheduler.amazonaws.com }`, ` Action: sts:AssumeRole`, ` Condition:`, ` StringEquals: { aws:SourceAccount: !Ref AWS::AccountId }`, ` Policies:`, ` - PolicyName: fire-forwarder`, ` PolicyDocument:`, ` Version: "2012-10-17"`, ` Statement:`, ` - Effect: Allow`, ` Action: lambda:InvokeFunction`, ` Resource: !GetAtt Forwarder.Arn`);
|
|
419
306
|
for (const { fact, expression } of translated) {
|
|
420
|
-
lines.push(``, ` Schedule${logicalId(fact.name)}:`, ` Type: AWS::Scheduler::Schedule`, ` Properties:`, ` Name: ${scheduleResourceName(input.name, fact.name)}`, ` ScheduleExpression: ${expression}`, ` ScheduleExpressionTimezone: ${fact.tz ?? "Etc/UTC"}`, ` FlexibleTimeWindow: { Mode: "OFF" }`, ` Target:`, ` Arn: !GetAtt Forwarder.Arn`, ` RoleArn: !GetAtt SchedulerRole.Arn`, ` # <aws.scheduler.scheduled-time>
|
|
307
|
+
lines.push(``, ` Schedule${logicalId(fact.name)}:`, ` Type: AWS::Scheduler::Schedule`, ` Properties:`, ` Name: ${scheduleResourceName(input.name, fact.name)}`, ` ScheduleExpression: ${expression}`, ` ScheduleExpressionTimezone: ${fact.tz ?? "Etc/UTC"}`, ` FlexibleTimeWindow: { Mode: "OFF" }`, ` Target:`, ` Arn: !GetAtt Forwarder.Arn`, ` RoleArn: !GetAtt SchedulerRole.Arn`, ` # <aws.scheduler.scheduled-time> is the clock's NAME for this occurrence: EventBridge repeats it`, ` # byte-identical on every redelivery, which is what makes the container's dedup work.`, ` Input: ${yamlSingleQuote(JSON.stringify({
|
|
308
|
+
scheduleFire: { name: fact.name, occurrence: "<aws.scheduler.scheduled-time>" },
|
|
309
|
+
}))}`);
|
|
421
310
|
}
|
|
422
311
|
}
|
|
423
312
|
lines.push(``, `Outputs:`, ` RuntimeArn:`, ` Value: !GetAtt Runtime.AgentRuntimeArn`);
|
|
424
|
-
|
|
425
|
-
lines.push(` ForwarderUrl:`, ` Value: !GetAtt ForwarderUrl.FunctionUrl`);
|
|
426
|
-
}
|
|
313
|
+
lines.push(` ForwarderUrl:`, ` Value: !GetAtt ForwarderUrl.FunctionUrl`);
|
|
427
314
|
return `${lines.join("\n")}\n`;
|
|
428
315
|
}
|
|
429
316
|
/** Compute the AgentCore deploy plan from the resolved definition. */
|
|
430
317
|
export function planAgentcoreDeploy(input) {
|
|
431
318
|
const { name, channels } = input;
|
|
432
|
-
const stack =
|
|
433
|
-
const repo =
|
|
319
|
+
const stack = agentcoreStackName(name);
|
|
320
|
+
const repo = agentcoreRepoName(name);
|
|
434
321
|
const prefix = input.agentPrefix;
|
|
435
|
-
// Translate every schedule; the ones EventBridge cannot express become explicit runbook warnings
|
|
436
|
-
// a schedule silently missing from the template would be the worst failure mode (nothing ever fires).
|
|
322
|
+
// Translate every schedule; the ones EventBridge cannot express become explicit runbook warnings.
|
|
437
323
|
const translated = [];
|
|
438
324
|
const untranslatable = [];
|
|
439
325
|
for (const fact of input.schedules) {
|
|
@@ -443,10 +329,9 @@ export function planAgentcoreDeploy(input) {
|
|
|
443
329
|
else
|
|
444
330
|
untranslatable.push({ name: fact.name, reason: result.error });
|
|
445
331
|
}
|
|
446
|
-
// Identifier collisions: the author-side → AWS-side name mappings are lossy (logical ids strip
|
|
447
|
-
//
|
|
448
|
-
//
|
|
449
|
-
// rename is the fix (a hash-mangled allocator would trade readability for an edge case).
|
|
332
|
+
// Identifier collisions: the author-side → AWS-side name mappings are lossy (logical ids strip punctuation;
|
|
333
|
+
// parameter names collapse underscores), so two DISTINCT legal inputs can land on one CloudFormation key — which
|
|
334
|
+
// would generate a silently wrong stack.
|
|
450
335
|
const logicalIds = new Map();
|
|
451
336
|
for (const { fact } of translated) {
|
|
452
337
|
const id = `Schedule${logicalId(fact.name)}`;
|
|
@@ -456,35 +341,21 @@ export function planAgentcoreDeploy(input) {
|
|
|
456
341
|
}
|
|
457
342
|
logicalIds.set(id, fact.name);
|
|
458
343
|
}
|
|
459
|
-
const
|
|
460
|
-
for (const s of deploymentSecrets(input.modelAuth, channels, input.extraSecrets)) {
|
|
461
|
-
const p = cfnParamName(s.name);
|
|
462
|
-
const clash = paramNames.get(p);
|
|
463
|
-
if (clash !== undefined && clash !== s.name) {
|
|
464
|
-
throw new Error(`secrets "${clash}" and "${s.name}" collapse to the same CloudFormation parameter (${p}) — rename one`);
|
|
465
|
-
}
|
|
466
|
-
paramNames.set(p, s.name);
|
|
467
|
-
}
|
|
468
|
-
const topology = agentcoreTopology(input, translated.length);
|
|
469
|
-
const needsForwarder = topology.forwarder;
|
|
344
|
+
const topology = agentcoreTopology(input);
|
|
470
345
|
const artifacts = [
|
|
471
346
|
{ path: `${prefix}${TEMPLATE_FILE}`, content: template(input, translated, topology) },
|
|
472
|
-
|
|
347
|
+
{ path: `${prefix}${FORWARDER_FILE}`, content: forwarderSource() },
|
|
473
348
|
...containerArtifacts(input),
|
|
474
349
|
];
|
|
475
|
-
const secrets =
|
|
476
|
-
const requiredSecrets = secrets.filter((s) => s.required);
|
|
477
|
-
const optionalSecrets = secrets.filter((s) => !s.required);
|
|
478
|
-
const paramHint = (list) => list.map((s) => `${cfnParamName(s.name)}=<value>`).join(" ");
|
|
350
|
+
const secrets = input.secrets ?? [];
|
|
479
351
|
const image = `<account-id>.dkr.ecr.<region>.amazonaws.com/${repo}:<tag>`;
|
|
480
|
-
const bucketHint =
|
|
352
|
+
const bucketHint = deploymentBucketName(name, "<account-id>");
|
|
481
353
|
const runbook = [
|
|
482
354
|
`# Deploy "${name}" to AWS Bedrock AgentCore. ${prefix}${TEMPLATE_FILE} / Dockerfile(.dockerignore) are generated above.`,
|
|
483
355
|
`# Prereqs: AWS CLI v2 with credentials + a region where AgentCore is available, and Docker with buildx`,
|
|
484
356
|
`# (the image MUST be linux/arm64 — the one host whose build runs on YOUR machine, not remotely).`,
|
|
485
357
|
``,
|
|
486
|
-
`# 1. ECR repository + the
|
|
487
|
-
`# the stack on purpose: it holds the agent's state snapshot, which must survive a delete-stack.`,
|
|
358
|
+
`# 1. ECR repository + the forwarder artifact bucket (one-time; skip what exists).`,
|
|
488
359
|
`aws ecr create-repository --repository-name ${repo}`,
|
|
489
360
|
`aws s3api create-bucket --bucket ${bucketHint} --region us-east-1 # us-east-1 ONLY`,
|
|
490
361
|
`aws s3api create-bucket --bucket ${bucketHint} --region <region> \\ # every OTHER region`,
|
|
@@ -492,79 +363,46 @@ export function planAgentcoreDeploy(input) {
|
|
|
492
363
|
`aws s3api put-public-access-block --bucket ${bucketHint} \\`,
|
|
493
364
|
` --public-access-block-configuration BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true`,
|
|
494
365
|
``,
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
`aws s3 cp ${prefix}lambda/forwarder.zip s3://${bucketHint}/forwarder/<hash>.zip`,
|
|
501
|
-
``,
|
|
502
|
-
]
|
|
503
|
-
: []),
|
|
366
|
+
`# 1b. Package the forwarder and upload it. Name the object by its CONTENT (a hash/date):`,
|
|
367
|
+
`# CloudFormation rolls the function only when the ForwarderS3Key VALUE changes.`,
|
|
368
|
+
`(cd ${prefix}lambda && zip -q forwarder.zip index.js)`,
|
|
369
|
+
`aws s3 cp ${prefix}lambda/forwarder.zip s3://${bucketHint}/forwarder/<hash>.zip`,
|
|
370
|
+
``,
|
|
504
371
|
`# 2. Build (linux/arm64) + push. Use a UNIQUE tag per deploy (a git sha / date): CloudFormation only`,
|
|
505
372
|
`# rolls the runtime when the ImageUri VALUE changes — re-pushing the same tag deploys nothing.`,
|
|
506
373
|
`aws ecr get-login-password | docker login --username AWS --password-stdin <account-id>.dkr.ecr.<region>.amazonaws.com`,
|
|
507
|
-
prefix
|
|
508
|
-
? `docker buildx build --platform linux/arm64 -f ${prefix}Dockerfile -t ${image} --push .`
|
|
509
|
-
: `docker buildx build --platform linux/arm64 -t ${image} --push .`,
|
|
374
|
+
`docker buildx build --platform linux/arm64 -f ${prefix}Dockerfile -t ${image} --push .`,
|
|
510
375
|
``,
|
|
511
|
-
`# 3. Deploy the stack (runtime + ingress + schedules in one template)
|
|
376
|
+
`# 3. Deploy the stack (runtime + ingress + schedules in one template).`,
|
|
512
377
|
];
|
|
513
|
-
if (
|
|
514
|
-
runbook.push(`#
|
|
515
|
-
}
|
|
516
|
-
if (optionalSecrets.length > 0) {
|
|
517
|
-
runbook.push(`# Optional parameters (set only when the matching feature is configured):`, ...optionalSecrets.map((s) => `# ${cfnParamName(s.name)}: ${s.hint}`));
|
|
518
|
-
}
|
|
519
|
-
const wakeSecretHint = input.selfSchedule ? " FastagentWakeSecret=<any random string>" : "";
|
|
520
|
-
if (input.selfSchedule) {
|
|
521
|
-
runbook.push(`# FastagentWakeSecret: the wake-alarm shared secret — any random string (\`--run\` mints one)`);
|
|
378
|
+
if (secrets.length > 0) {
|
|
379
|
+
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}`));
|
|
522
380
|
}
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
? [
|
|
529
|
-
`# The ingress transport is a separate Lambda and therefore a separate log source:`,
|
|
530
|
-
`fastagent logs agentcore --source forwarder --follow`,
|
|
531
|
-
]
|
|
532
|
-
: []));
|
|
533
|
-
// Model-auth guidance mirrors the other hosts: an env key became a parameter above; OAuth/stored
|
|
534
|
-
// can't be read at plan time — `--run` carries it as FastagentAuthSeed.
|
|
381
|
+
const wakeSecretHint = " FastagentWakeSecret=<any random string>";
|
|
382
|
+
runbook.push(`# FastagentWakeSecret: the wake-alarm shared secret — any random string (\`--run\` mints one)`);
|
|
383
|
+
runbook.push(`aws cloudformation deploy --stack-name ${stack} --template-file ${prefix}${TEMPLATE_FILE} \\`, ` --capabilities CAPABILITY_IAM \\`, ` --parameter-overrides ImageUri=${image} ForwarderBucket=${bucketHint} ForwarderS3Key=forwarder/<hash>.zip${secrets.length > 0 ? " FastagentEnv=<base64 JSON>" : ""}${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`);
|
|
384
|
+
// Model-auth guidance mirrors the other hosts: an env key became a parameter above; OAuth/stored can't be read at
|
|
385
|
+
// plan time — `--run` carries it as FastagentAuthSeed.
|
|
535
386
|
if (!isEnvKey(input.modelAuth)) {
|
|
536
387
|
runbook.push(``, input.modelAuth === undefined
|
|
537
|
-
? `# Model auth: none found at the local auth path —
|
|
388
|
+
? `# Model auth: none found at the local auth path — set FASTAGENT_AUTH_PATH, or \`--run\` carries it`
|
|
538
389
|
: `# Model auth: your local auth is "${input.modelAuth}" — the plan can't read its value; \`--run\` carries it`, `# as the FastagentAuthSeed parameter (base64 of auth.json), materialized on first boot.`);
|
|
539
390
|
}
|
|
540
|
-
// Post-deploy webhook registration — the shared channel-ingress steps, pointed at the forwarder's
|
|
541
|
-
//
|
|
542
|
-
// generate-only only warns and still prints this runbook, so the steps are filtered on ingress like
|
|
543
|
-
// every other host's rather than on the CLI having gated.
|
|
391
|
+
// Post-deploy webhook registration — the shared channel-ingress steps, pointed at the forwarder's Function URL
|
|
392
|
+
// (read from the stack outputs).
|
|
544
393
|
const post = webhookRunbook(`<ForwarderUrl>`, channels);
|
|
545
|
-
// AgentCore asides the shared steps cannot carry: nothing else routes through a forwarder, and
|
|
546
|
-
//
|
|
547
|
-
// Named rather than positional: the asides land after ALL the steps, so "the console" has to say
|
|
548
|
-
// WHICH console. One line for both kinds — an agent that declares feishu AND lark reads the same
|
|
549
|
-
// fact twice otherwise and looks for a second step that does not exist.
|
|
394
|
+
// AgentCore asides the shared steps cannot carry: nothing else routes through a forwarder, and nothing else has a
|
|
395
|
+
// compute ceiling.
|
|
550
396
|
if (webhookKinds(channels).some((kind) => kind === "feishu" || kind === "lark")) {
|
|
551
397
|
post.push(`# NOTE: the Feishu/Lark console's challenge rides through the forwarder to the channel and back`, `# verbatim, so the stack must be deployed when you save the Request URL.`);
|
|
552
398
|
}
|
|
553
|
-
if (channels.some((channel) => channel.name === "github")) {
|
|
554
|
-
post.push(`# NOTE: github turns are fire-and-forget with no replay — a compute reclaimed mid-review drops it`, `# (the ping's HealthyBusy + time_of_last_update holds the session while turns run, but the 8 h compute ceiling is hard).`);
|
|
555
|
-
}
|
|
556
399
|
if (post.length > 0)
|
|
557
400
|
runbook.push(``, ...post);
|
|
558
401
|
for (const u of untranslatable) {
|
|
559
402
|
runbook.push(``, `# WARNING: schedule "${u.name}" has NO EventBridge rule — ${u.reason}.`, `# It will NOT fire on this deployment until you create an equivalent trigger yourself.`);
|
|
560
403
|
}
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
}
|
|
564
|
-
runbook.push(``, `# Invoke the agent programmatically (any session id ≥ 33 chars; the response streams as SSE):`, `aws bedrock-agentcore invoke-agent-runtime --agent-runtime-arn <RuntimeArn> \\`, ` --runtime-session-id "my-conversation-000000000000000000" \\`, ` --payload '{"kind":"invoke","session":"cli","text":"hello"}' --cli-binary-format raw-in-base64-out /dev/stdout`);
|
|
565
|
-
if (needsForwarder) {
|
|
566
|
-
runbook.push(``, `# After a REDEPLOY, stop the ingress session so the new image serves immediately — a live session`, `# keeps its old compute (and the OLD image) until ${IDLE_TIMEOUT_SECONDS}s idle / the 8 h compute ceiling`, `# (\`--run\` does this automatically):`, `aws bedrock-agentcore stop-runtime-session --agent-runtime-arn <RuntimeArn> \\`, ` --runtime-session-id "${ingressSessionId(name)}"`);
|
|
567
|
-
}
|
|
568
|
-
runbook.push(``, `# Redeploy = step 1b (new forwarder key, if its code changed) + step 2 with a NEW tag + step 3.`, `# STATE: ${MOUNT} is a LOCAL disk — AWS wipes it on every runtime version update (i.e. every`, `# deploy) and after 14 idle days. What survives is the S3 snapshot under s3://${bucketHint}/${STATE_KEY}:`, `# the container restores it on its first invocation and pushes it whenever work settles. Keep that`, `# bucket and the agent keeps its sessions, channel state and pending wake-ups across deploys;`, `# delete it and the agent starts blank. (A persistent MOUNT would need EFS + VPC mode + a NAT`, `# gateway for model/channel egress — see the template comment.)`, `# CREDENTIALS RIDE THAT SNAPSHOT TOO: FASTAGENT_SECRETS_DIR is ${SECRETS_DIR}, inside the state`, `# root, so an OAuth auth.json ROTATED on the box persists (a refresh token is single-use — without`, `# this the next microVM would re-seed the deploy-time copy and eventually fail to authenticate).`, `# The bucket is therefore credential storage: it is created with public access blocked and`, `# versioning on, and deleting it costs model access until the next deploy re-seeds.`);
|
|
404
|
+
runbook.push(``, `# Wake-ups: the agent's own follow-up turns (the wake tool) are EventBridge-backed — each pending wake-up is mirrored`, `# (via the forwarder, authenticated by FastagentWakeSecret) into a self-deleting one-shot`, `# schedule (fa-${name}-wk-*) that wakes the container at the right instant. Reliable even when`, `# the compute is reclaimed. The forwarder supplies the callback URL; use the deployment's`, `# fixed runtime session id for programmatic calls too.`);
|
|
405
|
+
runbook.push(``, `# Invoke the agent programmatically (reuse the deployment session; conversations remain separate):`, `aws bedrock-agentcore invoke-agent-runtime --agent-runtime-arn <RuntimeArn> \\`, ` --runtime-session-id "${ingressSessionId(name)}" \\`, ` --payload '{"kind":"invoke","session":"cli","text":"hello"}' --cli-binary-format raw-in-base64-out /dev/stdout`);
|
|
406
|
+
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 base/ (including unfinished work), .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,`, `# and the model credential is re-seeded from FASTAGENT_AUTH_SEED. Deploying IS re-authenticating.`, `# An OAuth refresh token is single-use and shared with your machine, so the box can lose model`, `# access between deploys — deploy again to refresh it, or use a provider API key.`, `# 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 workspace; use the fixed runtime session id printed above for every entry point.`);
|
|
569
407
|
return { artifacts, runbook, untranslatableSchedules: untranslatable, topology };
|
|
570
408
|
}
|