@fastagent-sh/fastagent 0.21.1 → 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 -40
- 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,41 +1,31 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `fastagent deploy agentcore --run` — drive the AWS CLI + Docker to completion. The middle of the
|
|
3
|
-
* deploy the plain runbook hands to the operator; `--run` executes it so a coding agent runs ONE
|
|
4
|
-
* command. Idempotent (ECR check-then-act; `cloudformation deploy` converges the stack) and
|
|
5
|
-
* resumable: it STOPS at a human gate with one actionable line and a non-zero exit.
|
|
6
|
-
*
|
|
7
|
-
* TWO runners, one seam ({@link CliRunner}): `aws` (identity, ECR, CloudFormation) and `docker`
|
|
8
|
-
* (buildx). AgentCore is the ONE host whose image builds on the operator's machine — the platform
|
|
9
|
-
* requires linux/arm64 in the account's ECR and has no remote builder — so a missing Docker/buildx
|
|
10
|
-
* is a first-class gate, not an incidental failure.
|
|
11
|
-
*
|
|
12
|
-
* Secrets ride CloudFormation NoEcho parameters. `--parameter-overrides` on argv would put the
|
|
13
|
-
* values in the process listing (the same reason Fly imports secrets over stdin), so they go through
|
|
14
|
-
* a caller-provided temp parameters file (`file://…`, mode 0600, deleted by the caller) — the write
|
|
15
|
-
* is injected to keep this module pure and the security-sensitive wiring testable.
|
|
16
|
-
*/
|
|
1
|
+
/** `fastagent deploy agentcore --run` — drive the AWS CLI + Docker to completion. */
|
|
17
2
|
import { RESERVED_PATHS } from "../../channels/agentcore-protocol.js";
|
|
18
3
|
import { registerWebhooks } from "../channel-ingress.js";
|
|
4
|
+
import { awsCli, awsJson } from "./aws-cli.js";
|
|
19
5
|
import { createHash } from "node:crypto";
|
|
20
6
|
import { Buffer } from "node:buffer";
|
|
21
|
-
import {
|
|
7
|
+
import { CARRIERS, CARRIER_CHUNK_SIZE, CARRIER_MAX_CHUNKS, MOUNT, agentcoreRepoName, agentcoreStackName, carrierChunk, deploymentBucketName, forwarderSource, ingressSessionId, } from "./plan.js";
|
|
22
8
|
import { zipSingleFile } from "./zip.js";
|
|
23
|
-
|
|
24
|
-
* restore + channel construction) before gating with the last answer. */
|
|
25
|
-
const PROBE_TIMEOUT_MS = 120_000;
|
|
26
|
-
const PROBE_INTERVAL_MS = 3_000;
|
|
9
|
+
import { encodeCarriedEnv, missingValuesGate } from "../secrets.js";
|
|
27
10
|
/**
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* into an opaque 502, which would strip the very diagnostics this probe exists to carry.
|
|
33
|
-
*
|
|
34
|
-
* Outcome policy: `ok:true` verifies the deploy; `ok:false` gates IMMEDIATELY with the runtime's own
|
|
35
|
-
* error text (construction rejections are cached per session, so polling cannot change the answer);
|
|
36
|
-
* anything else (unroutable URL, forwarder 4xx/5xx, malformed body) is retried to the deadline —
|
|
37
|
-
* that budget's job is absorbing cold-start provisioning — and then gates with the last answer seen.
|
|
11
|
+
* Stack statuses that hold NO agent memory: a create that never succeeded, its rollback, a change set never
|
|
12
|
+
* executed, or a stack already gone. Everything else — including `UPDATE_ROLLBACK_COMPLETE` and any status AWS adds
|
|
13
|
+
* later — is treated as carrying state, so an unrecognized status over-warns instead of quietly promising nothing is
|
|
14
|
+
* lost.
|
|
38
15
|
*/
|
|
16
|
+
const EMPTY_STACK_STATUSES = new Set([
|
|
17
|
+
"CREATE_FAILED",
|
|
18
|
+
"CREATE_IN_PROGRESS",
|
|
19
|
+
"ROLLBACK_IN_PROGRESS",
|
|
20
|
+
"ROLLBACK_FAILED",
|
|
21
|
+
"ROLLBACK_COMPLETE",
|
|
22
|
+
"REVIEW_IN_PROGRESS",
|
|
23
|
+
"DELETE_COMPLETE",
|
|
24
|
+
]);
|
|
25
|
+
/** Budget for image pull, storage initialization and channel construction. */
|
|
26
|
+
const PROBE_TIMEOUT_MS = 240_000;
|
|
27
|
+
const PROBE_INTERVAL_MS = 3_000;
|
|
28
|
+
/** Drive the forwarder's reserved probe path until it answers, and read the runtime's STRUCTURED verdict. */
|
|
39
29
|
async function probeRuntime(probeUrl, auth, fetchImpl, timeoutMs = PROBE_TIMEOUT_MS, intervalMs = PROBE_INTERVAL_MS) {
|
|
40
30
|
const deadline = Date.now() + timeoutMs;
|
|
41
31
|
let last;
|
|
@@ -54,7 +44,7 @@ async function probeRuntime(probeUrl, auth, fetchImpl, timeoutMs = PROBE_TIMEOUT
|
|
|
54
44
|
verdict = JSON.parse(bodyText);
|
|
55
45
|
}
|
|
56
46
|
catch {
|
|
57
|
-
|
|
47
|
+
// malformed — fall through to retry with it as the last answer
|
|
58
48
|
}
|
|
59
49
|
if (verdict?.ok === true)
|
|
60
50
|
return { ok: true };
|
|
@@ -67,7 +57,7 @@ async function probeRuntime(probeUrl, auth, fetchImpl, timeoutMs = PROBE_TIMEOUT
|
|
|
67
57
|
last = `${res.status}${firstLine ? ` ${firstLine}` : ""}`;
|
|
68
58
|
}
|
|
69
59
|
catch {
|
|
70
|
-
|
|
60
|
+
// not routable yet (Function URL DNS, cold start) — keep polling until the deadline
|
|
71
61
|
}
|
|
72
62
|
if (Date.now() >= deadline) {
|
|
73
63
|
return {
|
|
@@ -81,90 +71,79 @@ async function probeRuntime(probeUrl, auth, fetchImpl, timeoutMs = PROBE_TIMEOUT
|
|
|
81
71
|
}
|
|
82
72
|
}
|
|
83
73
|
/** Stack outputs (`describe-stacks --query "Stacks[0].Outputs"`) → { OutputKey: OutputValue }. */
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
96
|
-
catch {
|
|
74
|
+
/**
|
|
75
|
+
* `Stacks[0].Outputs` as a map, or `undefined` when the document is not that list at all.
|
|
76
|
+
*
|
|
77
|
+
* `null` IS AN ANSWER, and an empty map is what it says: that is what JMESPath prints for a stack with no
|
|
78
|
+
* Outputs section, i.e. a template edited or generated without one. Reading it as unreadable blamed
|
|
79
|
+
* `aws cloudformation describe-stacks` — a command that had just succeeded — instead of letting the caller's
|
|
80
|
+
* own "no RuntimeArn, regenerate with --force" gate say what to do. Distinct from empty stdout, which is the
|
|
81
|
+
* CLI printing nothing at all (aws-cli.ts, point 4).
|
|
82
|
+
*/
|
|
83
|
+
export function pickStackOutputs(parsed) {
|
|
84
|
+
if (parsed === null)
|
|
97
85
|
return {};
|
|
86
|
+
if (!Array.isArray(parsed))
|
|
87
|
+
return undefined;
|
|
88
|
+
const out = {};
|
|
89
|
+
for (const o of parsed) {
|
|
90
|
+
if (typeof o?.OutputKey === "string" && typeof o?.OutputValue === "string")
|
|
91
|
+
out[o.OutputKey] = o.OutputValue;
|
|
98
92
|
}
|
|
93
|
+
return out;
|
|
94
|
+
}
|
|
95
|
+
/** The secrets `--run` mints for this host, each with the template parameter of its own. */
|
|
96
|
+
const MINTED_PARAMS = {
|
|
97
|
+
FASTAGENT_INGRESS_SECRET: "FastagentIngressSecret",
|
|
98
|
+
FASTAGENT_WAKE_SECRET: "FastagentWakeSecret",
|
|
99
|
+
};
|
|
100
|
+
/** What each carrier holds for this deploy: the credential seed, and every other variable as one encoded object. */
|
|
101
|
+
function carrierValues(secrets) {
|
|
102
|
+
const { FASTAGENT_AUTH_SEED: seed = "", ...rest } = secrets;
|
|
103
|
+
const carried = Object.fromEntries(Object.entries(rest).filter(([name]) => !(name in MINTED_PARAMS)));
|
|
104
|
+
return { FASTAGENT_AUTH_SEED: seed, FASTAGENT_ENV: encodeCarriedEnv(carried) };
|
|
99
105
|
}
|
|
100
|
-
/** The `--parameter-overrides file://` payload: a JSON array of "Key=Value" strings.
|
|
101
|
-
* is CHUNKED across FastagentAuthSeed(2…) — AgentCore env values cap at 2048 chars and a real OAuth
|
|
102
|
-
* auth.json's base64 exceeds it; `start` reassembles (collectAuthSeed). Every chunk is emitted on
|
|
103
|
-
* every deploy, including empty trailing chunks, so CloudFormation cannot retain stale values. */
|
|
106
|
+
/** The `--parameter-overrides file://` payload: a JSON array of "Key=Value" strings. */
|
|
104
107
|
export function paramsFileContent(imageUri, secrets, forwarder) {
|
|
105
|
-
const params = [`ImageUri=${imageUri}`];
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
if (k !== "FASTAGENT_AUTH_SEED")
|
|
110
|
-
params.push(`${cfnParamName(k)}=${v}`);
|
|
108
|
+
const params = [`ImageUri=${imageUri}`, `ForwarderBucket=${forwarder.bucket}`, `ForwarderS3Key=${forwarder.key}`];
|
|
109
|
+
for (const [name, param] of Object.entries(MINTED_PARAMS)) {
|
|
110
|
+
if (secrets[name] !== undefined)
|
|
111
|
+
params.push(`${param}=${secrets[name]}`);
|
|
111
112
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
}
|
|
119
|
-
/** Parse the runtime's checkpoint acknowledgement without treating malformed output as success. */
|
|
120
|
-
export function parseCheckpointReply(stdout) {
|
|
121
|
-
try {
|
|
122
|
-
const parsed = JSON.parse(stdout.trim());
|
|
123
|
-
if (parsed === null ||
|
|
124
|
-
typeof parsed !== "object" ||
|
|
125
|
-
!("written" in parsed) ||
|
|
126
|
-
typeof parsed.written !== "boolean") {
|
|
127
|
-
return undefined;
|
|
113
|
+
// Every chunk is written, empty ones included: an omitted parameter keeps its PREVIOUS value on a stack update.
|
|
114
|
+
const values = carrierValues(secrets);
|
|
115
|
+
for (const carrier of CARRIERS) {
|
|
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)}`);
|
|
128
119
|
}
|
|
129
|
-
return {
|
|
130
|
-
written: parsed.written,
|
|
131
|
-
reason: "reason" in parsed && typeof parsed.reason === "string" ? parsed.reason : undefined,
|
|
132
|
-
};
|
|
133
|
-
}
|
|
134
|
-
catch {
|
|
135
|
-
return undefined;
|
|
136
120
|
}
|
|
121
|
+
return `${JSON.stringify(params)}\n`;
|
|
137
122
|
}
|
|
138
|
-
/**
|
|
139
|
-
* Run the deploy through `aws` + `docker`. `log` reports progress; the injected registrars perform
|
|
140
|
-
* post-deploy webhook steps from the builder machine against the forwarder's Function URL. Every
|
|
141
|
-
* gate is fail-visible; `writeSecretFile` is the caller's 0600-temp-file seam (see the header).
|
|
142
|
-
*/
|
|
123
|
+
/** Run the deploy through `aws` + `docker`. */
|
|
143
124
|
export async function deployAgentcoreRun(plan, aws, docker, log, writeSecretFile, writeForwarderZip, registrars,
|
|
144
125
|
/** Injected in tests; the probe itself stays inside the run so no deploy can skip it. */
|
|
145
126
|
probe = {}) {
|
|
146
127
|
const gate = (g) => ({ ok: false, gate: g });
|
|
147
|
-
const stack =
|
|
148
|
-
const repo =
|
|
149
|
-
// 1.
|
|
150
|
-
|
|
151
|
-
const identity = await
|
|
152
|
-
|
|
153
|
-
return
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
}
|
|
165
|
-
catch {
|
|
166
|
-
return gate("could not read the account id from `aws sts get-caller-identity` — see the output above");
|
|
128
|
+
const stack = agentcoreStackName(plan.name);
|
|
129
|
+
const repo = agentcoreRepoName(plan.name);
|
|
130
|
+
// 1.
|
|
131
|
+
const cli = awsCli(aws);
|
|
132
|
+
const identity = await cli.read(["sts", "get-caller-identity", "--output", "json"], awsJson((parsed) => {
|
|
133
|
+
const { Account, Arn } = (parsed ?? {});
|
|
134
|
+
return typeof Account === "string"
|
|
135
|
+
? { account: Account, principal: typeof Arn === "string" ? Arn : undefined }
|
|
136
|
+
: undefined;
|
|
137
|
+
}));
|
|
138
|
+
if (!("ok" in identity)) {
|
|
139
|
+
if ("absent" in identity)
|
|
140
|
+
return gate("`aws sts get-caller-identity` found no caller — run `aws configure`");
|
|
141
|
+
if (identity.code === 127)
|
|
142
|
+
return gate(`${identity.unreadable}, then re-run`);
|
|
143
|
+
// QUOTED: expired token, a missing profile and a proxy that eats STS are three different next actions.
|
|
144
|
+
return gate(`no working AWS credentials (${identity.unreadable}) — run \`aws configure\`, then re-run`);
|
|
167
145
|
}
|
|
146
|
+
const { account, principal } = identity.ok;
|
|
168
147
|
let region = plan.region;
|
|
169
148
|
if (!region) {
|
|
170
149
|
const fromConfig = await aws(["configure", "get", "region"], { capture: true });
|
|
@@ -173,10 +152,7 @@ probe = {}) {
|
|
|
173
152
|
if (!region) {
|
|
174
153
|
return gate("no AWS region configured — set AWS_REGION (or `aws configure set region <region>`), then re-run");
|
|
175
154
|
}
|
|
176
|
-
// 2.
|
|
177
|
-
// no remote builder), so their absence is a first-class gate with the install pointer. ANY
|
|
178
|
-
// non-zero gates BEFORE side effects: `docker version` with the daemon down exits non-127, and
|
|
179
|
-
// letting it through would create the ECR repo and then fail the build with a generic error.
|
|
155
|
+
// 2.
|
|
180
156
|
const dockerVersion = await docker(["version"], { capture: true });
|
|
181
157
|
if (dockerVersion.code === 127) {
|
|
182
158
|
return gate("docker not found — install Docker (https://docs.docker.com/get-docker/), then re-run");
|
|
@@ -188,29 +164,91 @@ probe = {}) {
|
|
|
188
164
|
return gate("docker buildx not available — the image must be linux/arm64 (cross-built); install buildx, then re-run");
|
|
189
165
|
}
|
|
190
166
|
// 3. Gate missing required secret VALUES before any side effect (no half-created infra).
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
// 3b.
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
return gate(`your auth.json is too large to carry (${seed.length} chars base64 > ${AUTH_SEED_CHUNK_SIZE * AUTH_SEED_MAX_CHUNKS}) — ` +
|
|
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_AUTH_SEED.length > capacity) {
|
|
174
|
+
return gate(`your auth.json is too large to carry (${carried.FASTAGENT_AUTH_SEED.length} chars base64 > ${capacity}) — ` +
|
|
200
175
|
`slim it (keep only the model's credential), or set a provider API key in .env instead`);
|
|
201
176
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
}
|
|
177
|
+
if (carried.FASTAGENT_ENV.length > capacity) {
|
|
178
|
+
return gate(`the variables in ${plan.valueFile} are too large to carry (${carried.FASTAGENT_ENV.length} chars encoded > ` +
|
|
179
|
+
`${capacity}) — move what the deployment does not need out of that file`);
|
|
206
180
|
}
|
|
207
|
-
//
|
|
208
|
-
|
|
209
|
-
|
|
181
|
+
// Name what travels from the value file onto the runtime.
|
|
182
|
+
const secretNames = Object.keys(plan.secrets);
|
|
183
|
+
if (secretNames.length > 0)
|
|
184
|
+
log(`carrying ${secretNames.length} secret(s): ${secretNames.join(", ")}`);
|
|
185
|
+
// 3c. Say what this deploy is ABOUT TO TOUCH while it can still be stopped for free: until here the account and
|
|
186
|
+
// region only surfaced several hundred log lines in, inside the ECR image URI.
|
|
187
|
+
const source = plan.region ? "the environment" : "aws configure";
|
|
188
|
+
log(`account ${account}${principal ? ` (${principal})` : ""}, region ${region} (from ${source}), image ${repo}:${plan.tag}`);
|
|
189
|
+
if (principal?.endsWith(":root")) {
|
|
190
|
+
log(`warn: deploying as the account root user — this stack's IAM roles are created under it; prefer an IAM role`);
|
|
191
|
+
}
|
|
192
|
+
// Warn, never gate: an aws CLI too old to know the service, or a role without ListAgentRuntimes, would make a gate
|
|
193
|
+
// refuse a valid deploy. The point is to say it BEFORE the multi-minute arm64 build, not to be authoritative.
|
|
194
|
+
// `--region` explicitly: every other region-dependent call here writes it out (the ECR registry URI, the bucket's
|
|
195
|
+
// LocationConstraint), and the warning below names ${region} — the probe must be about the SAME region it names.
|
|
196
|
+
const service = await cli.present([
|
|
197
|
+
"bedrock-agentcore-control",
|
|
198
|
+
"list-agent-runtimes",
|
|
199
|
+
"--max-items",
|
|
200
|
+
"1",
|
|
201
|
+
"--region",
|
|
202
|
+
region,
|
|
203
|
+
]);
|
|
204
|
+
if (!("ok" in service)) {
|
|
205
|
+
const why = "unreadable" in service ? service.unreadable.split("\n")[0] : "";
|
|
206
|
+
log(`warn: could not confirm AgentCore is available in ${region}${why ? ` (${why})` : ""} — if it is not, ` +
|
|
207
|
+
`cloudformation deploy fails minutes from now, after the image build: ` +
|
|
208
|
+
`https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/agentcore-regions.html`);
|
|
209
|
+
}
|
|
210
|
+
// 3d. Read the stack here, for the two questions that both depend on it: does this deploy destroy the agent's
|
|
211
|
+
// memory (below), and is there a failed first create to clear (step 7)? The destructive one is only worth saying
|
|
212
|
+
// while the multi-minute build has not run yet, and step 7 reuses this answer unless it was still in flight.
|
|
213
|
+
// An unanswered question must not read as "a first deploy, nothing to lose": `sts get-caller-identity` succeeding
|
|
214
|
+
// says nothing about whether this role can read CloudFormation. That three-way answer is aws-cli.ts's.
|
|
215
|
+
const readStackStatus = () => cli.read([
|
|
216
|
+
"cloudformation",
|
|
217
|
+
"describe-stacks",
|
|
218
|
+
"--stack-name",
|
|
219
|
+
stack,
|
|
220
|
+
"--query",
|
|
221
|
+
"Stacks[0].StackStatus",
|
|
222
|
+
"--output",
|
|
223
|
+
"text",
|
|
224
|
+
], (stdout) => stdout.trim() || undefined);
|
|
225
|
+
const stackStatus = await readStackStatus();
|
|
226
|
+
const status = "ok" in stackStatus ? stackStatus.ok : "";
|
|
227
|
+
// The only answers the build can invalidate: one still in flight (a first create rolling back is exactly what step 7
|
|
228
|
+
// exists for, and minutes of arm64 build are long enough for it to settle), and one we never got.
|
|
229
|
+
const settling = status.endsWith("_IN_PROGRESS") || "unreadable" in stackStatus;
|
|
230
|
+
// Warn, never gate — same as the region probe above: a role without this read, or an older CLI, must not refuse a
|
|
231
|
+
// legitimate deploy.
|
|
232
|
+
if ("unreadable" in stackStatus) {
|
|
233
|
+
log(`warn: could not read stack ${stack} (${stackStatus.unreadable}) — if it exists, this deploy resets its ` +
|
|
234
|
+
`managed SessionStorage (${MOUNT}) and a failed first create will not be cleared`);
|
|
235
|
+
}
|
|
236
|
+
else if (status !== "" && !EMPTY_STACK_STATUSES.has(status)) {
|
|
237
|
+
log(`warn: this is a REDEPLOY and AWS resets managed SessionStorage (${MOUNT}) on every runtime version update — ` +
|
|
238
|
+
`sessions, channel state and pending wake-ups start blank` +
|
|
239
|
+
// Only the carried auth.json is re-seeded; a provider API key deployment has no such step to blame.
|
|
240
|
+
`${carried.FASTAGENT_AUTH_SEED ? ", and the model credential is re-seeded from FASTAGENT_AUTH_SEED" : ""}. ` +
|
|
241
|
+
`Cross-deploy memory needs a real volume: \`deploy fly\` or \`deploy railway\`.`);
|
|
242
|
+
}
|
|
243
|
+
// 4.
|
|
210
244
|
const registry = `${account}.dkr.ecr.${region}.amazonaws.com`;
|
|
211
245
|
const image = `${registry}/${repo}:${plan.tag}`;
|
|
212
|
-
const described = await
|
|
213
|
-
if (
|
|
246
|
+
const described = await cli.present(["ecr", "describe-repositories", "--repository-names", repo]);
|
|
247
|
+
if ("unreadable" in described) {
|
|
248
|
+
// NOT the create branch: a denial here must name this step, not the create that would follow.
|
|
249
|
+
return gate(`could not read ECR repository ${repo} (${described.unreadable}) — fix that, then re-run`);
|
|
250
|
+
}
|
|
251
|
+
if ("ok" in described) {
|
|
214
252
|
log(`ECR repository ${repo} exists — skipping create`);
|
|
215
253
|
}
|
|
216
254
|
else {
|
|
@@ -219,84 +257,43 @@ probe = {}) {
|
|
|
219
257
|
return gate("`aws ecr create-repository` failed — see the output above; fix and re-run");
|
|
220
258
|
}
|
|
221
259
|
}
|
|
222
|
-
//
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
//
|
|
226
|
-
//
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
// CONVERGE the properties on EVERY deploy, not just at creation. They are what makes the bucket
|
|
241
|
-
// safe (nothing public) and recoverable (a bad write is not the end of the agent's memory); doing
|
|
242
|
-
// them only in the create branch means a run that failed halfway leaves a bucket that looks
|
|
243
|
-
// finished forever after, and ignoring the exit codes means "deployed" would be reported over a
|
|
244
|
-
// world-readable or unversioned store of the agent's credentials.
|
|
245
|
-
const converge = [
|
|
246
|
-
{
|
|
247
|
-
label: "block public access",
|
|
248
|
-
args: [
|
|
249
|
-
"s3api",
|
|
250
|
-
"put-public-access-block",
|
|
251
|
-
"--bucket",
|
|
252
|
-
bucket,
|
|
253
|
-
"--public-access-block-configuration",
|
|
254
|
-
"BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true",
|
|
255
|
-
],
|
|
256
|
-
},
|
|
257
|
-
{
|
|
258
|
-
label: "enable versioning",
|
|
259
|
-
args: ["s3api", "put-bucket-versioning", "--bucket", bucket, "--versioning-configuration", "Status=Enabled"],
|
|
260
|
-
},
|
|
261
|
-
{
|
|
262
|
-
label: "set the snapshot lifecycle",
|
|
263
|
-
args: [
|
|
264
|
-
"s3api",
|
|
265
|
-
"put-bucket-lifecycle-configuration",
|
|
266
|
-
"--bucket",
|
|
267
|
-
bucket,
|
|
268
|
-
"--lifecycle-configuration",
|
|
269
|
-
JSON.stringify({
|
|
270
|
-
Rules: [
|
|
271
|
-
{
|
|
272
|
-
ID: "fastagent-expire-old-snapshots",
|
|
273
|
-
Status: "Enabled",
|
|
274
|
-
Filter: { Prefix: "state/" },
|
|
275
|
-
NoncurrentVersionExpiration: { NoncurrentDays: 7 },
|
|
276
|
-
},
|
|
277
|
-
],
|
|
278
|
-
}),
|
|
279
|
-
],
|
|
280
|
-
},
|
|
281
|
-
];
|
|
282
|
-
for (const step of converge) {
|
|
283
|
-
// `capture` keeps the CLI's JSON off the deploy log: the exit code is the signal, and a failure
|
|
284
|
-
// gates with the step's name below.
|
|
285
|
-
if ((await aws(step.args, { capture: true })).code !== 0) {
|
|
286
|
-
return gate(`could not ${step.label} on ${bucket} — refusing to store agent state in it; fix and re-run`);
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
// Content-hashed key: CloudFormation rolls the function only when a parameter VALUE changes, so
|
|
290
|
-
// identical source must map to an identical key (hence the deterministic zip) and changed source
|
|
291
|
-
// to a new one.
|
|
292
|
-
const zip = zipSingleFile("index.js", Buffer.from(forwarderSource()));
|
|
293
|
-
const key = `forwarder/${createHash("sha256").update(zip).digest("hex").slice(0, 16)}.zip`;
|
|
294
|
-
const zipPath = await writeForwarderZip(zip);
|
|
295
|
-
if ((await aws(["s3", "cp", zipPath, `s3://${bucket}/${key}`])).code !== 0) {
|
|
296
|
-
return gate("uploading the forwarder package to S3 failed — see the output above; fix and re-run");
|
|
260
|
+
// The forwarder package must exist before CloudFormation can create its Lambda.
|
|
261
|
+
const bucket = deploymentBucketName(plan.name, account);
|
|
262
|
+
const head = await cli.present(["s3api", "head-bucket", "--bucket", bucket]);
|
|
263
|
+
// Same shape as the repository above: a 403 is not a 404, and creating on top of it fails with a message
|
|
264
|
+
// about the wrong thing.
|
|
265
|
+
if ("unreadable" in head) {
|
|
266
|
+
return gate(`could not read deployment bucket ${bucket} (${head.unreadable}) — fix that, then re-run`);
|
|
267
|
+
}
|
|
268
|
+
if ("absent" in head) {
|
|
269
|
+
log(`creating deployment bucket ${bucket}…`);
|
|
270
|
+
// us-east-1 is the ONE region that must not carry a LocationConstraint (the API rejects it).
|
|
271
|
+
const createArgs = ["s3api", "create-bucket", "--bucket", bucket];
|
|
272
|
+
if (region !== "us-east-1")
|
|
273
|
+
createArgs.push("--create-bucket-configuration", `LocationConstraint=${region}`);
|
|
274
|
+
if ((await aws(createArgs)).code !== 0) {
|
|
275
|
+
return gate(`\`aws s3api create-bucket --bucket ${bucket}\` failed — see the output above; fix and re-run`);
|
|
297
276
|
}
|
|
298
|
-
forwarderParams = { bucket, key };
|
|
299
277
|
}
|
|
278
|
+
if ((await aws([
|
|
279
|
+
"s3api",
|
|
280
|
+
"put-public-access-block",
|
|
281
|
+
"--bucket",
|
|
282
|
+
bucket,
|
|
283
|
+
"--public-access-block-configuration",
|
|
284
|
+
"BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true",
|
|
285
|
+
], { capture: true })).code !== 0) {
|
|
286
|
+
return gate(`could not block public access on ${bucket}; fix and re-run`);
|
|
287
|
+
}
|
|
288
|
+
// Content-hashed key: CloudFormation rolls the function only when a parameter VALUE changes, so identical source
|
|
289
|
+
// must map to an identical key (hence the deterministic zip) and changed source to a new one.
|
|
290
|
+
const zip = zipSingleFile("index.js", Buffer.from(forwarderSource()));
|
|
291
|
+
const key = `forwarder/${createHash("sha256").update(zip).digest("hex").slice(0, 16)}.zip`;
|
|
292
|
+
const zipPath = await writeForwarderZip(zip);
|
|
293
|
+
if ((await aws(["s3", "cp", zipPath, `s3://${bucket}/${key}`])).code !== 0) {
|
|
294
|
+
return gate("uploading the forwarder package to S3 failed — see the output above; fix and re-run");
|
|
295
|
+
}
|
|
296
|
+
const forwarderParams = { bucket, key };
|
|
300
297
|
// 5. Registry login — the password flows stdout→stdin between the two runners, never argv.
|
|
301
298
|
const password = await aws(["ecr", "get-login-password"], { capture: true });
|
|
302
299
|
if (password.code !== 0)
|
|
@@ -307,29 +304,14 @@ probe = {}) {
|
|
|
307
304
|
// 6. Build (linux/arm64) + push in one step.
|
|
308
305
|
log(`building + pushing ${image} (linux/arm64)…`);
|
|
309
306
|
const buildArgs = ["buildx", "build", "--platform", "linux/arm64", "-t", image, "--push"];
|
|
310
|
-
|
|
311
|
-
buildArgs.push("-f", plan.dockerfilePath);
|
|
307
|
+
buildArgs.push("-f", plan.dockerfilePath);
|
|
312
308
|
buildArgs.push(".");
|
|
313
309
|
if ((await docker(buildArgs)).code !== 0) {
|
|
314
310
|
return gate("`docker buildx build` failed — see the output above; fix and re-run");
|
|
315
311
|
}
|
|
316
|
-
// 7.
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
// ROLLBACK_COMPLETE, which CloudFormation refuses to update — without this, "fix and re-run"
|
|
320
|
-
// (our own gate advice) would dead-end on a different error. Nothing real is lost by deleting:
|
|
321
|
-
// a ROLLBACK_COMPLETE stack holds no live resources.
|
|
322
|
-
const status = await aws([
|
|
323
|
-
"cloudformation",
|
|
324
|
-
"describe-stacks",
|
|
325
|
-
"--stack-name",
|
|
326
|
-
stack,
|
|
327
|
-
"--query",
|
|
328
|
-
"Stacks[0].StackStatus",
|
|
329
|
-
"--output",
|
|
330
|
-
"text",
|
|
331
|
-
], { capture: true });
|
|
332
|
-
if (status.code === 0 && status.stdout.trim() === "ROLLBACK_COMPLETE") {
|
|
312
|
+
// 7. The 3d answer stands unless it was still settling then — a second read is the narrow exception, not the rule.
|
|
313
|
+
const beforeDeploy = settling ? await readStackStatus() : stackStatus;
|
|
314
|
+
if ("ok" in beforeDeploy && beforeDeploy.ok === "ROLLBACK_COMPLETE") {
|
|
333
315
|
log(`stack ${stack} is ROLLBACK_COMPLETE (a failed first create) — deleting it before re-creating…`);
|
|
334
316
|
if ((await aws(["cloudformation", "delete-stack", "--stack-name", stack])).code !== 0) {
|
|
335
317
|
return gate("`aws cloudformation delete-stack` failed — see the output above");
|
|
@@ -358,66 +340,18 @@ probe = {}) {
|
|
|
358
340
|
`(aws cloudformation describe-stack-events --stack-name ${stack}), fix, and re-run`);
|
|
359
341
|
}
|
|
360
342
|
// 8. Outputs — the runtime ARN (the data plane) and the forwarder URL (the webhook surface).
|
|
361
|
-
const
|
|
362
|
-
if (
|
|
363
|
-
|
|
364
|
-
|
|
343
|
+
const outputsRead = await cli.read(["cloudformation", "describe-stacks", "--stack-name", stack, "--query", "Stacks[0].Outputs", "--output", "json"], awsJson(pickStackOutputs));
|
|
344
|
+
if (!("ok" in outputsRead)) {
|
|
345
|
+
const why = "absent" in outputsRead ? `${stack} is not there` : outputsRead.unreadable;
|
|
346
|
+
return gate(`\`aws cloudformation describe-stacks\` failed after a successful deploy (${why})`);
|
|
347
|
+
}
|
|
348
|
+
const outputs = outputsRead.ok;
|
|
365
349
|
const runtimeArn = outputs.RuntimeArn;
|
|
366
350
|
if (!runtimeArn)
|
|
367
351
|
return gate("stack has no RuntimeArn output — was the template edited? Regenerate with --force");
|
|
368
352
|
const url = outputs.ForwarderUrl?.replace(/\/$/, ""); // registrars append /<path>; no double slash
|
|
369
|
-
//
|
|
370
|
-
|
|
371
|
-
// redeploy "succeeds" while an actively-chatting session keeps answering from the PREVIOUS
|
|
372
|
-
// image (the exact silent trap the first real deploy hit). Failure is advisory, never a gate:
|
|
373
|
-
// on a first deploy the session does not exist yet, and the stop is an immediacy optimization
|
|
374
|
-
// — the platform's reclaim gets there eventually. An in-flight turn on the old compute is cut;
|
|
375
|
-
// the checkpoint above is what lets a replaying channel re-run it. Only when a forwarder exists
|
|
376
|
-
// (the ingress session is the forwarder's session; pure-invoke deployments have none).
|
|
377
|
-
// Keyed on the FORWARDER: every current forwarder has a callback URL for state-capability refresh,
|
|
378
|
-
// and every forwarder topology has an ingress session whose next event would otherwise land on compute still
|
|
379
|
-
// running the previous image.
|
|
380
|
-
if (plan.topology.forwarder) {
|
|
381
|
-
// CHECKPOINT FIRST. The stop cuts whatever turn is running, and that turn's durable intent was
|
|
382
|
-
// written to a mount the version update erases — so without this flush "replay re-runs it" would
|
|
383
|
-
// be false: the intent never reaches S3 and the message is simply gone. Best-effort: a session
|
|
384
|
-
// that is not up has nothing to lose, and a failure here must not block the (already applied)
|
|
385
|
-
// deploy — it only downgrades the promise, so say so.
|
|
386
|
-
const checkpointPayloadPath = await writeSecretFile(`${JSON.stringify({ kind: "checkpoint", auth: plan.secrets.FASTAGENT_INGRESS_SECRET })}\n`);
|
|
387
|
-
const checkpoint = await aws([
|
|
388
|
-
"bedrock-agentcore",
|
|
389
|
-
"invoke-agent-runtime",
|
|
390
|
-
"--agent-runtime-arn",
|
|
391
|
-
runtimeArn,
|
|
392
|
-
"--runtime-session-id",
|
|
393
|
-
ingressSessionId(plan.name),
|
|
394
|
-
"--payload",
|
|
395
|
-
`file://${checkpointPayloadPath}`,
|
|
396
|
-
"--cli-binary-format",
|
|
397
|
-
"raw-in-base64-out",
|
|
398
|
-
"/dev/stdout",
|
|
399
|
-
], { capture: true, captureStderr: true });
|
|
400
|
-
// Report what the container ACTUALLY did. This line is the only signal an operator has about
|
|
401
|
-
// whether an in-flight turn survived the deploy, so a blanket "checkpointed" — printed even when
|
|
402
|
-
// nothing was written — would be worse than no line at all.
|
|
403
|
-
if (checkpoint.code !== 0) {
|
|
404
|
-
log("note: could not reach the ingress session to checkpoint — if a turn was in flight it is lost " +
|
|
405
|
-
"rather than replayed (see the output above)");
|
|
406
|
-
}
|
|
407
|
-
else {
|
|
408
|
-
const reply = parseCheckpointReply(checkpoint.stdout);
|
|
409
|
-
if (reply?.written) {
|
|
410
|
-
log("checkpointed the ingress session (an interrupted turn can be replayed)");
|
|
411
|
-
}
|
|
412
|
-
else if (reply) {
|
|
413
|
-
// The ordinary case: the session was already idle-reclaimed, so its snapshot was written when
|
|
414
|
-
// it settled and there is nothing in flight to lose.
|
|
415
|
-
log(`note: nothing to checkpoint${reply.reason ? ` — ${reply.reason}` : " (no session was running)"}`);
|
|
416
|
-
}
|
|
417
|
-
else {
|
|
418
|
-
log("warn: ingress session returned an invalid checkpoint response — could not verify the state snapshot");
|
|
419
|
-
}
|
|
420
|
-
}
|
|
353
|
+
// A live session keeps its previous image. Stop the fixed writer before verifying the new release.
|
|
354
|
+
{
|
|
421
355
|
log("stopping the ingress session so the new image serves immediately…");
|
|
422
356
|
const stopCommand = [
|
|
423
357
|
"bedrock-agentcore",
|
|
@@ -427,21 +361,19 @@ probe = {}) {
|
|
|
427
361
|
"--runtime-session-id",
|
|
428
362
|
ingressSessionId(plan.name),
|
|
429
363
|
];
|
|
430
|
-
const stopped = await
|
|
431
|
-
if (
|
|
432
|
-
// Classify, don't guess: "no session yet" (first deploy — expected, quiet note) vs a REAL stop
|
|
433
|
-
//
|
|
434
|
-
//
|
|
435
|
-
|
|
436
|
-
const stderr =
|
|
437
|
-
if (
|
|
364
|
+
const stopped = await cli.write(stopCommand);
|
|
365
|
+
if (!("done" in stopped)) {
|
|
366
|
+
// Classify, don't guess: "no session yet" (first deploy — expected, quiet note) vs a REAL stop failure
|
|
367
|
+
// (permissions/CLI/network), which must stop verification against the previous image. Both words are
|
|
368
|
+
// aws-cli.ts's — this file had its own third spelling of "already gone".
|
|
369
|
+
const noSession = "absent" in stopped;
|
|
370
|
+
const stderr = noSession ? "" : stopped.refused;
|
|
371
|
+
if (noSession) {
|
|
438
372
|
log("note: no ingress session to stop (first deploy, or already reclaimed)");
|
|
439
373
|
}
|
|
440
374
|
else {
|
|
441
|
-
// A GATE, not a warning: the probe below reaches the SAME fixed session id, so a session
|
|
442
|
-
//
|
|
443
|
-
// verified a serving path it never touched. Unable to guarantee the session is fresh =
|
|
444
|
-
// unable to verify = stop.
|
|
375
|
+
// A GATE, not a warning: the probe below reaches the SAME fixed session id, so a session still running the
|
|
376
|
+
// previous image would answer it and the deploy would claim to have verified a serving path it never touched.
|
|
445
377
|
const firstLine = stderr.trim().split("\n")[0];
|
|
446
378
|
return gate(`could not stop the ingress session — it may still be serving the PREVIOUS image, so the ` +
|
|
447
379
|
`deploy cannot verify the new one${firstLine ? ` (${firstLine})` : ""}. ` +
|
|
@@ -449,31 +381,20 @@ probe = {}) {
|
|
|
449
381
|
}
|
|
450
382
|
}
|
|
451
383
|
}
|
|
452
|
-
// 8c.
|
|
453
|
-
|
|
454
|
-
// check (there is no boot-time failStartup on this host). A missing output means an edited
|
|
455
|
-
// template; skipping the probe silently would let such a deploy report success unverified.
|
|
456
|
-
// Only a pure-invoke deployment (no forwarder) legitimately has no URL and nothing to probe.
|
|
457
|
-
if (plan.topology.forwarder && !url) {
|
|
384
|
+
// 8c.
|
|
385
|
+
if (!url) {
|
|
458
386
|
return gate("this deployment needs the forwarder but the stack has no ForwarderUrl output — regenerate the " +
|
|
459
387
|
"template with --force");
|
|
460
388
|
}
|
|
461
|
-
//
|
|
462
|
-
// session on the new image through the forwarder's reserved path, which restores the state
|
|
463
|
-
// snapshot and constructs the channels — construction is deferred to exactly that moment
|
|
464
|
-
// (channels/agentcore.ts), so this is where a bad credential, a broken channels/ module, or an
|
|
465
|
-
// unrestorable snapshot surfaces AT DEPLOY TIME with the runtime's own error text.
|
|
389
|
+
// Verify storage initialization and channel construction before registering webhooks.
|
|
466
390
|
if (url) {
|
|
467
|
-
log("probing the deployed runtime (
|
|
391
|
+
log("probing the deployed runtime (workspace initialization + channel construction)…");
|
|
468
392
|
const verdict = await probeRuntime(`${url}${RESERVED_PATHS.probe}`, plan.secrets.FASTAGENT_INGRESS_SECRET ?? "", probe.fetchImpl ?? fetch, probe.timeoutMs, probe.intervalMs);
|
|
469
393
|
if (!verdict.ok)
|
|
470
394
|
return gate(verdict.gate);
|
|
471
|
-
log("runtime verified (
|
|
395
|
+
log("runtime verified (workspace ready, channels constructed)");
|
|
472
396
|
}
|
|
473
|
-
// 9.
|
|
474
|
-
// forwarder's Function URL. Gate policy is the shared registration-gate kernel.
|
|
475
|
-
// No long-connection channels reach here: `deploy.ts` gates them before the driver runs (AgentCore
|
|
476
|
-
// has no resident process to hold a connection), so the deploy's channels are all webhook ones.
|
|
397
|
+
// 9.
|
|
477
398
|
const registrationGateMsg = url
|
|
478
399
|
? await registerWebhooks({
|
|
479
400
|
baseUrl: url,
|