@fastagent-sh/fastagent 0.29.0 → 0.30.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 -4
- package/dist/atomic-write.d.ts +4 -5
- package/dist/atomic-write.js +4 -5
- package/dist/authoring.d.ts +32 -15
- package/dist/authoring.js +147 -127
- package/dist/channels/agentcore-protocol.d.ts +8 -3
- package/dist/channels/agentcore-protocol.js +3 -1
- package/dist/channels/agentcore-service.d.ts +1 -0
- package/dist/channels/agentcore-service.js +14 -4
- package/dist/channels/agentcore.d.ts +9 -0
- package/dist/channels/agentcore.js +29 -4
- package/dist/channels/context.d.ts +2 -0
- package/dist/channels/context.js +5 -1
- package/dist/channels/feishu/feishu-api.d.ts +2 -1
- package/dist/channels/feishu/feishu.d.ts +0 -3
- package/dist/channels/feishu/feishu.js +10 -10
- package/dist/channels/feishu/register-app.js +3 -5
- package/dist/channels/feishu/scaffold/channel.ts +32 -34
- package/dist/channels/feishu/scaffold/feishu-send.ts +5 -5
- package/dist/channels/feishu/scaffold/feishu-threads.ts +3 -3
- package/dist/channels/feishu/setup-mode.d.ts +2 -12
- package/dist/channels/feishu/setup-mode.js +2 -22
- package/dist/channels/feishu/setup.d.ts +12 -0
- package/dist/channels/feishu/setup.js +197 -0
- package/dist/channels/kit/abortable.d.ts +8 -0
- package/dist/channels/kit/abortable.js +22 -0
- package/dist/channels/lark/lark.d.ts +0 -3
- package/dist/channels/lark/lark.js +0 -5
- package/dist/channels/lark/scaffold/channel.ts +33 -35
- package/dist/channels/lark/scaffold/lark-send.ts +5 -5
- package/dist/channels/lark/scaffold/lark-threads.ts +3 -3
- package/dist/channels/lark/setup.d.ts +9 -0
- package/dist/channels/lark/setup.js +57 -0
- package/dist/channels/setup.d.ts +112 -0
- package/dist/channels/setup.js +31 -0
- package/dist/channels/slack/history.js +1 -1
- package/dist/channels/slack/manifest.d.ts +17 -7
- package/dist/channels/slack/manifest.js +12 -16
- package/dist/channels/slack/scaffold/channel.ts +44 -30
- package/dist/channels/slack/scaffold/slack-send.ts +8 -8
- package/dist/channels/slack/scaffold/slack-threads.ts +4 -4
- package/dist/channels/slack/setup.d.ts +7 -0
- package/dist/channels/slack/setup.js +119 -0
- package/dist/channels/slack/slack-api.d.ts +2 -0
- package/dist/channels/slack/slack-api.js +4 -0
- package/dist/channels/slack/slack.d.ts +21 -2
- package/dist/channels/slack/slack.js +398 -338
- package/dist/channels/slack/socket.d.ts +57 -0
- package/dist/channels/slack/socket.js +305 -0
- package/dist/channels/telegram/polling.d.ts +27 -0
- package/dist/channels/telegram/polling.js +105 -0
- package/dist/channels/telegram/scaffold/channel.ts +52 -32
- package/dist/channels/telegram/scaffold/telegram-send.ts +9 -9
- package/dist/channels/telegram/setup.d.ts +2 -0
- package/dist/channels/telegram/setup.js +42 -0
- package/dist/channels/telegram/telegram-api.d.ts +27 -1
- package/dist/channels/telegram/telegram-api.js +7 -6
- package/dist/channels/telegram/telegram.d.ts +11 -3
- package/dist/channels/telegram/telegram.js +184 -157
- package/dist/channels/webhook-evidence.d.ts +11 -0
- package/dist/channels/webhook-evidence.js +71 -0
- package/dist/channels/webhooks.d.ts +37 -0
- package/dist/channels/webhooks.js +70 -0
- package/dist/child-env.d.ts +16 -0
- package/dist/child-env.js +30 -0
- package/dist/cli/box-login.js +4 -1
- package/dist/cli/channel-app.d.ts +3 -0
- package/dist/cli/channel-app.js +35 -0
- package/dist/cli/channel-setup.d.ts +38 -0
- package/dist/cli/channel-setup.js +195 -0
- package/dist/cli/commands/add.d.ts +6 -14
- package/dist/cli/commands/add.js +45 -303
- package/dist/cli/commands/auth.js +8 -2
- package/dist/cli/commands/build.d.ts +5 -0
- package/dist/cli/commands/build.js +16 -0
- package/dist/cli/commands/channels.d.ts +19 -0
- package/dist/cli/commands/channels.js +230 -0
- package/dist/cli/commands/content.d.ts +7 -3
- package/dist/cli/commands/content.js +16 -10
- package/dist/cli/commands/deploy/agentcore.js +55 -23
- package/dist/cli/commands/deploy/channel-apps.d.ts +17 -0
- package/dist/cli/commands/deploy/channel-apps.js +30 -0
- package/dist/cli/commands/deploy/docker.js +32 -65
- package/dist/cli/commands/deploy/fly.js +28 -12
- package/dist/cli/commands/deploy/railway.js +38 -13
- package/dist/cli/commands/deploy/secrets-step.d.ts +19 -0
- package/dist/cli/commands/deploy/secrets-step.js +66 -0
- package/dist/cli/commands/deploy/shared.d.ts +16 -5
- package/dist/cli/commands/deploy/shared.js +25 -8
- package/dist/cli/commands/deploy.js +53 -60
- package/dist/cli/commands/dev.d.ts +2 -0
- package/dist/cli/commands/dev.js +15 -3
- package/dist/cli/commands/env.js +1 -1
- package/dist/cli/commands/info.d.ts +2 -0
- package/dist/cli/commands/info.js +60 -25
- package/dist/cli/commands/init.js +3 -1
- package/dist/cli/commands/invoke.d.ts +2 -0
- package/dist/cli/commands/invoke.js +20 -7
- package/dist/cli/commands/login.d.ts +0 -7
- package/dist/cli/commands/login.js +22 -10
- package/dist/cli/commands/logout.js +1 -1
- package/dist/cli/commands/mcp.js +14 -6
- package/dist/cli/commands/models.js +3 -2
- package/dist/cli/commands/schedules.d.ts +4 -2
- package/dist/cli/commands/schedules.js +14 -10
- package/dist/cli/commands/secrets.d.ts +14 -0
- package/dist/cli/commands/secrets.js +213 -0
- package/dist/cli/commands/start.d.ts +14 -14
- package/dist/cli/commands/start.js +46 -60
- package/dist/cli/content-view.d.ts +4 -6
- package/dist/cli/content-view.js +5 -11
- package/dist/cli/first-run-channels.d.ts +3 -0
- package/dist/cli/first-run-channels.js +63 -0
- package/dist/cli/instance.d.ts +41 -0
- package/dist/cli/instance.js +155 -0
- package/dist/cli/invoke-stream.d.ts +5 -2
- package/dist/cli/invoke-stream.js +7 -2
- package/dist/cli/program.js +282 -110
- package/dist/cli/serve.d.ts +4 -5
- package/dist/cli/serve.js +4 -7
- package/dist/cli/shared.d.ts +11 -2
- package/dist/cli/shared.js +32 -19
- package/dist/cli/ticket.js +50 -6
- package/dist/content/declare.d.ts +31 -37
- package/dist/content/declare.js +59 -60
- package/dist/content/file.d.ts +23 -12
- package/dist/content/file.js +48 -35
- package/dist/content/git.d.ts +0 -10
- package/dist/content/git.js +5 -39
- package/dist/content/resolve.d.ts +18 -22
- package/dist/content/resolve.js +32 -66
- package/dist/content/source.d.ts +7 -8
- package/dist/content/source.js +10 -28
- package/dist/credentials-page.d.ts +26 -0
- package/dist/credentials-page.js +185 -0
- package/dist/declared-secrets.d.ts +10 -3
- package/dist/declared-secrets.js +8 -4
- package/dist/deploy/agentcore/destroy.d.ts +4 -1
- package/dist/deploy/agentcore/destroy.js +25 -1
- package/dist/deploy/agentcore/forwarder.js +1 -1
- package/dist/deploy/agentcore/plan.d.ts +7 -21
- package/dist/deploy/agentcore/plan.js +47 -49
- package/dist/deploy/agentcore/run.d.ts +18 -10
- package/dist/deploy/agentcore/run.js +93 -70
- package/dist/deploy/agentcore/runtime-secrets.d.ts +7 -0
- package/dist/deploy/agentcore/runtime-secrets.js +108 -0
- package/dist/deploy/agentcore/shell.js +12 -19
- package/dist/deploy/agentcore/sigv4.d.ts +7 -0
- package/dist/deploy/agentcore/sigv4.js +15 -0
- package/dist/deploy/agentcore/slr.d.ts +18 -0
- package/dist/deploy/agentcore/slr.js +27 -0
- package/dist/deploy/agentcore/store.d.ts +7 -0
- package/dist/deploy/agentcore/store.js +115 -0
- package/dist/deploy/api.d.ts +14 -14
- package/dist/deploy/api.js +26 -24
- package/dist/deploy/box-shell.d.ts +3 -1
- package/dist/deploy/build-context.d.ts +4 -4
- package/dist/deploy/build-context.js +11 -15
- package/dist/deploy/channel-ingress.d.ts +35 -9
- package/dist/deploy/channel-ingress.js +119 -22
- package/dist/deploy/container.d.ts +35 -10
- package/dist/deploy/container.js +79 -37
- package/dist/deploy/delivered-text.d.ts +13 -0
- package/dist/deploy/delivered-text.js +86 -0
- package/dist/deploy/docker/plan.d.ts +7 -8
- package/dist/deploy/docker/plan.js +31 -52
- package/dist/deploy/docker/run.d.ts +5 -43
- package/dist/deploy/docker/run.js +9 -84
- package/dist/deploy/file-store.d.ts +2 -0
- package/dist/deploy/file-store.js +17 -0
- package/dist/deploy/fly/plan.d.ts +3 -2
- package/dist/deploy/fly/plan.js +20 -9
- package/dist/deploy/fly/run.d.ts +18 -8
- package/dist/deploy/fly/run.js +24 -42
- package/dist/deploy/fly/store.d.ts +12 -0
- package/dist/deploy/fly/store.js +83 -0
- package/dist/deploy/hosts.d.ts +1 -2
- package/dist/deploy/hosts.js +1 -2
- package/dist/deploy/preflight.d.ts +18 -5
- package/dist/deploy/preflight.js +135 -63
- package/dist/deploy/railway/ignore.d.ts +8 -0
- package/dist/deploy/railway/ignore.js +57 -0
- package/dist/deploy/railway/plan.d.ts +1 -2
- package/dist/deploy/railway/plan.js +8 -6
- package/dist/deploy/railway/run.d.ts +21 -15
- package/dist/deploy/railway/run.js +32 -78
- package/dist/deploy/railway/store.d.ts +29 -0
- package/dist/deploy/railway/store.js +143 -0
- package/dist/deploy/runner.d.ts +5 -2
- package/dist/deploy/runner.js +4 -3
- package/dist/deploy/secret-store.d.ts +63 -0
- package/dist/deploy/secret-store.js +98 -0
- package/dist/deploy/secrets.d.ts +29 -46
- package/dist/deploy/secrets.js +49 -122
- package/dist/deploy/workspace.d.ts +6 -12
- package/dist/deploy/workspace.js +18 -94
- package/dist/dev-supervisor.js +6 -8
- package/dist/environment/declare.js +3 -2
- package/dist/environment/mise.d.ts +3 -1
- package/dist/environment/mise.js +4 -2
- package/dist/feishu.d.ts +1 -1
- package/dist/feishu.js +1 -1
- package/dist/harnesses/models/catalog.d.ts +1 -0
- package/dist/harnesses/models/catalog.js +1 -2
- package/dist/harnesses/models/environment.d.ts +7 -2
- package/dist/harnesses/models/environment.js +9 -5
- package/dist/harnesses/models/host.d.ts +0 -7
- package/dist/harnesses/models/host.js +10 -1
- package/dist/harnesses/models/login.d.ts +0 -8
- package/dist/harnesses/models/login.js +5 -4
- package/dist/harnesses/models/models.d.ts +7 -2
- package/dist/harnesses/models/models.js +28 -4
- package/dist/harnesses/pi-durable/agent.js +59 -38
- package/dist/harnesses/pi-durable/build.d.ts +12 -0
- package/dist/harnesses/pi-durable/build.js +270 -0
- package/dist/harnesses/pi-durable/built.d.ts +24 -0
- package/dist/harnesses/pi-durable/built.js +136 -0
- package/dist/harnesses/pi-durable/config.d.ts +81 -0
- package/dist/harnesses/pi-durable/config.js +266 -0
- package/dist/harnesses/pi-durable/define.d.ts +63 -13
- package/dist/harnesses/pi-durable/define.js +23 -2
- package/dist/harnesses/pi-durable/definition.d.ts +72 -0
- package/dist/harnesses/pi-durable/definition.js +143 -0
- package/dist/harnesses/pi-durable/events.d.ts +4 -3
- package/dist/harnesses/pi-durable/events.js +179 -136
- package/dist/harnesses/pi-durable/execution-env.d.ts +17 -0
- package/dist/harnesses/pi-durable/execution-env.js +11 -0
- package/dist/harnesses/pi-durable/mcp.js +1 -3
- package/dist/harnesses/pi-durable/open.d.ts +35 -37
- package/dist/harnesses/pi-durable/open.js +91 -153
- package/dist/harnesses/pi-durable/reads.d.ts +0 -1
- package/dist/harnesses/pi-durable/reads.js +1 -3
- package/dist/harnesses/pi-durable/registry.d.ts +4 -6
- package/dist/harnesses/pi-durable/registry.js +29 -26
- package/dist/harnesses/pi-durable/release.d.ts +2 -2
- package/dist/harnesses/pi-durable/release.js +10 -7
- package/dist/harnesses/pi-durable/serve.d.ts +91 -15
- package/dist/harnesses/pi-durable/serve.js +109 -47
- package/dist/harnesses/pi-durable/sessions.js +15 -12
- package/dist/harnesses/pi-durable/text-sources.d.ts +30 -0
- package/dist/harnesses/pi-durable/text-sources.js +55 -0
- package/dist/harnesses/pi-durable/text.d.ts +16 -4
- package/dist/harnesses/pi-durable/text.js +115 -59
- package/dist/harnesses/pi-durable/wake.js +8 -8
- package/dist/http/client.js +9 -3
- package/dist/http/contract.d.ts +3 -3
- package/dist/http/server.d.ts +1 -1
- package/dist/http/server.js +1 -1
- package/dist/index.d.ts +8 -5
- package/dist/index.js +4 -1
- package/dist/instance-name.d.ts +7 -0
- package/dist/instance-name.js +26 -0
- package/dist/lark.d.ts +1 -1
- package/dist/lark.js +1 -1
- package/dist/mcp/client.js +11 -2
- package/dist/mcp/config.d.ts +70 -18
- package/dist/mcp/config.js +136 -53
- package/dist/mcp/host.d.ts +5 -2
- package/dist/mcp/host.js +14 -6
- package/dist/mcp/sign-in.js +2 -2
- package/dist/mcp-api.d.ts +14 -0
- package/dist/paths.d.ts +22 -8
- package/dist/paths.js +44 -11
- package/dist/runtime-websocket.d.ts +14 -0
- package/dist/runtime-websocket.js +39 -0
- package/dist/scaffold/add-channel.d.ts +9 -16
- package/dist/scaffold/add-channel.js +13 -83
- package/dist/scaffold/templates/env.example +4 -5
- package/dist/scaffold/templates/fastagent.config.ts +20 -14
- package/dist/scaffold/templates/gitignore +9 -6
- package/dist/scaffold/templates.d.ts +6 -2
- package/dist/scaffold/templates.js +7 -2
- package/dist/scaffold/vendor-skill.js +4 -2
- package/dist/service.d.ts +44 -6
- package/dist/service.js +110 -10
- package/dist/serving-command.d.ts +10 -0
- package/dist/serving-command.js +1 -0
- package/dist/slack.d.ts +1 -1
- package/dist/slack.js +1 -1
- package/dist/strip-edges.d.ts +10 -0
- package/dist/strip-edges.js +22 -0
- package/dist/telegram.d.ts +1 -1
- package/dist/telegram.js +1 -1
- package/dist/tunnel.d.ts +0 -1
- package/dist/tunnel.js +2 -8
- package/dist/value-file.d.ts +11 -6
- package/dist/value-file.js +21 -9
- package/dist/version.d.ts +7 -1
- package/dist/version.js +9 -3
- package/package.json +6 -5
- package/dist/channels/lark/credentials-page.d.ts +0 -25
- package/dist/channels/lark/credentials-page.js +0 -206
- package/dist/channels/lark/onboard.d.ts +0 -52
- package/dist/channels/lark/onboard.js +0 -86
- package/dist/channels/slack/config-api.d.ts +0 -69
- package/dist/channels/slack/config-api.js +0 -162
- package/dist/channels/slack/onboard.d.ts +0 -40
- package/dist/channels/slack/onboard.js +0 -136
- package/dist/channels/slack/onboarding-state.d.ts +0 -31
- package/dist/channels/slack/onboarding-state.js +0 -64
- package/dist/channels/slack/register-webhook.d.ts +0 -12
- package/dist/channels/slack/register-webhook.js +0 -67
- package/dist/channels/slack/setup-server.d.ts +0 -32
- package/dist/channels/slack/setup-server.js +0 -246
- package/dist/cli/add-feishu.d.ts +0 -41
- package/dist/cli/add-feishu.js +0 -391
- package/dist/cli/add-slack.d.ts +0 -18
- package/dist/cli/add-slack.js +0 -216
- package/dist/harnesses/pi-durable/parked.d.ts +0 -18
- package/dist/harnesses/pi-durable/parked.js +0 -27
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/** `fastagent deploy agentcore --run` — drive the AWS CLI + Docker to completion. */
|
|
2
2
|
import type { BoxLoginStep } from "../box-shell.ts";
|
|
3
3
|
import type { DeclaredChannel } from "../../channels/discover.ts";
|
|
4
|
-
import { type Registrars } from "../channel-ingress.ts";
|
|
4
|
+
import { type Registrars, type UrlRecorder } from "../channel-ingress.ts";
|
|
5
5
|
import type { CliRunner } from "../runner.ts";
|
|
6
6
|
import { type AgentcoreTopology } from "./plan.ts";
|
|
7
|
+
import type { SecretStore } from "../secret-store.ts";
|
|
7
8
|
export interface AgentcoreRunPlan {
|
|
8
9
|
/** The base name — stack `fastagent-<name>`, ECR repo `fastagent/<name>`. */
|
|
9
10
|
name: string;
|
|
@@ -18,12 +19,12 @@ export interface AgentcoreRunPlan {
|
|
|
18
19
|
* region`.
|
|
19
20
|
*/
|
|
20
21
|
region?: string;
|
|
21
|
-
/**
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
|
|
22
|
+
/** Makes the secret hold every value the deployment needs (secrets-step.ts), or says what is missing. */
|
|
23
|
+
provideSecrets: (store: SecretStore) => Promise<string | undefined>;
|
|
24
|
+
/** What proves an envelope came from the forwarder: minted by the deploy, a stack parameter. */
|
|
25
|
+
ingressSecret: string;
|
|
26
|
+
/** The wake-alarm shared secret between the container and the forwarder: minted by the deploy, a stack parameter. */
|
|
27
|
+
wakeSecret: string;
|
|
27
28
|
/** Every declared channel and its ingress — the driver asks which of them have a webhook. */
|
|
28
29
|
channels: readonly DeclaredChannel[];
|
|
29
30
|
/**
|
|
@@ -32,10 +33,14 @@ export interface AgentcoreRunPlan {
|
|
|
32
33
|
*/
|
|
33
34
|
topology: AgentcoreTopology;
|
|
34
35
|
/**
|
|
35
|
-
* Log the runtime in (`fastagent login --deployment`) once the probe verified it and before
|
|
36
|
-
*
|
|
36
|
+
* Log the runtime in (`fastagent login --deployment`) once the probe verified it and before the deploy reads its
|
|
37
|
+
* webhooks. Resolves a gate line, or undefined.
|
|
37
38
|
*/
|
|
38
39
|
boxLogin?: BoxLoginStep<[runtimeArn: string]>;
|
|
40
|
+
/** The `http.publicUrl` the configuration names (a custom domain); default the forwarder's URL. */
|
|
41
|
+
publicUrl?: string;
|
|
42
|
+
/** At a terminal with a `--config` file: record each URL a person set once the box confirms it. */
|
|
43
|
+
recordUrl?: UrlRecorder;
|
|
39
44
|
}
|
|
40
45
|
export type AgentcoreRunOutcome = {
|
|
41
46
|
ok: true;
|
|
@@ -57,7 +62,10 @@ export type AgentcoreRunOutcome = {
|
|
|
57
62
|
*/
|
|
58
63
|
export declare function pickStackOutputs(parsed: unknown): Record<string, string> | undefined;
|
|
59
64
|
/** The `--parameter-overrides file://` payload: a JSON array of "Key=Value" strings. */
|
|
60
|
-
export declare function paramsFileContent(imageUri: string,
|
|
65
|
+
export declare function paramsFileContent(imageUri: string, secretArn: string, minted: {
|
|
66
|
+
ingressSecret: string;
|
|
67
|
+
wakeSecret: string;
|
|
68
|
+
}, forwarder: {
|
|
61
69
|
bucket: string;
|
|
62
70
|
key: string;
|
|
63
71
|
}): string;
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { RESERVED_PATHS } from "../../channels/agentcore-protocol.js";
|
|
2
|
-
import { loginGate,
|
|
2
|
+
import { confirmWebhooks, loginGate, } from "../channel-ingress.js";
|
|
3
3
|
import { awsCli, awsJson } from "./aws-cli.js";
|
|
4
4
|
import { createHash } from "node:crypto";
|
|
5
5
|
import { Buffer } from "node:buffer";
|
|
6
|
-
import {
|
|
6
|
+
import { MOUNT, agentcoreRepoName, agentcoreSecretName, agentcoreStackName, deploymentBucketName, forwarderSource, ingressSessionId, } from "./plan.js";
|
|
7
7
|
import { zipSingleFile } from "./zip.js";
|
|
8
|
-
import {
|
|
8
|
+
import { RUNTIME_IDENTITY_ROLE, ensureServiceLinkedRole } from "./slr.js";
|
|
9
|
+
import { agentcoreSecretStore } from "./store.js";
|
|
9
10
|
/** AWS keys are typed, so a coding agent's user signs the CLI in at a terminal of their own. */
|
|
10
11
|
const AWS_SIGN_IN = "run `aws configure` (or `aws sso login`) in a terminal of your own, where the keys are typed, then re-run";
|
|
11
12
|
/**
|
|
@@ -23,23 +24,51 @@ const EMPTY_STACK_STATUSES = new Set([
|
|
|
23
24
|
"REVIEW_IN_PROGRESS",
|
|
24
25
|
"DELETE_COMPLETE",
|
|
25
26
|
]);
|
|
27
|
+
/**
|
|
28
|
+
* How often the deploy asks the runtime how its webhooks went. Each ask is a Lambda and an `InvokeAgentRuntime` that
|
|
29
|
+
* keeps the microVM up and is billed, where another host's `/health` costs nothing.
|
|
30
|
+
*/
|
|
31
|
+
const WEBHOOKS_INTERVAL_MS = 5_000;
|
|
26
32
|
/** Budget for image pull, storage initialization and channel construction. */
|
|
27
33
|
const PROBE_TIMEOUT_MS = 240_000;
|
|
28
34
|
const PROBE_INTERVAL_MS = 3_000;
|
|
35
|
+
/** One call to the forwarder's reserved probe path. */
|
|
36
|
+
async function askProbe(probeUrl, auth, fetchImpl) {
|
|
37
|
+
const res = await fetchImpl(probeUrl, {
|
|
38
|
+
method: "POST",
|
|
39
|
+
headers: { "content-type": "application/json" },
|
|
40
|
+
body: JSON.stringify({ auth }),
|
|
41
|
+
signal: AbortSignal.timeout(65_000),
|
|
42
|
+
});
|
|
43
|
+
return { status: res.status, body: await res.text() };
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The runtime's own report of its webhook channels, through the probe: the container has no `/health` of its own to
|
|
47
|
+
* read, and the forwarder relays only this path. The address it is handed is the other hosts' `/health`, which this
|
|
48
|
+
* one has no use for.
|
|
49
|
+
*/
|
|
50
|
+
function forwarderWebhooks(probeUrl, auth, fetchImpl) {
|
|
51
|
+
return async () => {
|
|
52
|
+
try {
|
|
53
|
+
const { status, body } = await askProbe(probeUrl, auth, fetchImpl);
|
|
54
|
+
const verdict = status === 200 ? JSON.parse(body) : undefined;
|
|
55
|
+
return verdict?.ok === true && Array.isArray(verdict.webhooks)
|
|
56
|
+
? verdict.webhooks
|
|
57
|
+
: undefined;
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return undefined; // polled again until the deadline
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
}
|
|
29
64
|
/** Drive the forwarder's reserved probe path until it answers, and read the runtime's STRUCTURED verdict. */
|
|
30
65
|
async function probeRuntime(probeUrl, auth, fetchImpl, timeoutMs = PROBE_TIMEOUT_MS, intervalMs = PROBE_INTERVAL_MS) {
|
|
31
66
|
const deadline = Date.now() + timeoutMs;
|
|
32
67
|
let last;
|
|
33
68
|
for (;;) {
|
|
34
69
|
try {
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
headers: { "content-type": "application/json" },
|
|
38
|
-
body: JSON.stringify({ auth }),
|
|
39
|
-
signal: AbortSignal.timeout(65_000),
|
|
40
|
-
});
|
|
41
|
-
const bodyText = await res.text();
|
|
42
|
-
if (res.status === 200) {
|
|
70
|
+
const { status, body: bodyText } = await askProbe(probeUrl, auth, fetchImpl);
|
|
71
|
+
if (status === 200) {
|
|
43
72
|
let verdict;
|
|
44
73
|
try {
|
|
45
74
|
verdict = JSON.parse(bodyText);
|
|
@@ -55,7 +84,7 @@ async function probeRuntime(probeUrl, auth, fetchImpl, timeoutMs = PROBE_TIMEOUT
|
|
|
55
84
|
}
|
|
56
85
|
}
|
|
57
86
|
const firstLine = bodyText.trim().split("\n")[0] ?? "";
|
|
58
|
-
last = `${
|
|
87
|
+
last = `${status}${firstLine ? ` ${firstLine}` : ""}`;
|
|
59
88
|
}
|
|
60
89
|
catch {
|
|
61
90
|
// not routable yet (Function URL DNS, cold start) — keep polling until the deadline
|
|
@@ -93,32 +122,16 @@ export function pickStackOutputs(parsed) {
|
|
|
93
122
|
}
|
|
94
123
|
return out;
|
|
95
124
|
}
|
|
96
|
-
/** The secrets `--run` mints for this host, each with the template parameter of its own. */
|
|
97
|
-
const MINTED_PARAMS = {
|
|
98
|
-
FASTAGENT_INGRESS_SECRET: "FastagentIngressSecret",
|
|
99
|
-
FASTAGENT_WAKE_SECRET: "FastagentWakeSecret",
|
|
100
|
-
};
|
|
101
|
-
/** What each carrier holds for this deploy: every variable but the minted ones, as one encoded object. */
|
|
102
|
-
function carrierValues(secrets) {
|
|
103
|
-
const carried = Object.fromEntries(Object.entries(secrets).filter(([name]) => !(name in MINTED_PARAMS)));
|
|
104
|
-
return { FASTAGENT_ENV: encodeCarriedEnv(carried) };
|
|
105
|
-
}
|
|
106
125
|
/** The `--parameter-overrides file://` payload: a JSON array of "Key=Value" strings. */
|
|
107
|
-
export function paramsFileContent(imageUri,
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
const value = values[carrier.env];
|
|
117
|
-
for (let i = 0; i < CARRIER_MAX_CHUNKS; i++) {
|
|
118
|
-
params.push(`${carrierChunk(carrier, i).param}=${value.slice(i * CARRIER_CHUNK_SIZE, (i + 1) * CARRIER_CHUNK_SIZE)}`);
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
return `${JSON.stringify(params)}\n`;
|
|
126
|
+
export function paramsFileContent(imageUri, secretArn, minted, forwarder) {
|
|
127
|
+
return `${JSON.stringify([
|
|
128
|
+
`ImageUri=${imageUri}`,
|
|
129
|
+
`SecretArn=${secretArn}`,
|
|
130
|
+
`ForwarderBucket=${forwarder.bucket}`,
|
|
131
|
+
`ForwarderS3Key=${forwarder.key}`,
|
|
132
|
+
`FastagentIngressSecret=${minted.ingressSecret}`,
|
|
133
|
+
`FastagentWakeSecret=${minted.wakeSecret}`,
|
|
134
|
+
])}\n`;
|
|
122
135
|
}
|
|
123
136
|
/** Run the deploy through `aws` + `docker`. */
|
|
124
137
|
export async function deployAgentcoreRun(plan, aws, docker, log, writeSecretFile, writeForwarderZip, registrars,
|
|
@@ -163,21 +176,6 @@ probe = {}) {
|
|
|
163
176
|
if ((await docker(["buildx", "version"], { capture: true })).code !== 0) {
|
|
164
177
|
return gate("docker buildx not available — the image must be linux/arm64 (cross-built); install buildx, then re-run");
|
|
165
178
|
}
|
|
166
|
-
// 3. Gate missing required secret VALUES before any side effect (no half-created infra).
|
|
167
|
-
const missingValues = missingValuesGate(plan.missingSecrets, plan.valueFile);
|
|
168
|
-
if (missingValues)
|
|
169
|
-
return gate(missingValues);
|
|
170
|
-
// 3b.
|
|
171
|
-
const capacity = CARRIER_CHUNK_SIZE * CARRIER_MAX_CHUNKS;
|
|
172
|
-
const carried = carrierValues(plan.secrets);
|
|
173
|
-
if (carried.FASTAGENT_ENV.length > capacity) {
|
|
174
|
-
return gate(`the variables in ${plan.valueFile} are too large to carry (${carried.FASTAGENT_ENV.length} chars encoded > ` +
|
|
175
|
-
`${capacity}) — move what the deployment does not need out of that file`);
|
|
176
|
-
}
|
|
177
|
-
// Name what travels from the value file onto the runtime.
|
|
178
|
-
const secretNames = Object.keys(plan.secrets);
|
|
179
|
-
if (secretNames.length > 0)
|
|
180
|
-
log(`carrying ${secretNames.length} secret(s): ${secretNames.join(", ")}`);
|
|
181
179
|
// 3c. Say what this deploy is ABOUT TO TOUCH while it can still be stopped for free: until here the account and
|
|
182
180
|
// region only surfaced several hundred log lines in, inside the ECR image URI.
|
|
183
181
|
const source = plan.region ? "the environment" : "aws configure";
|
|
@@ -203,7 +201,12 @@ probe = {}) {
|
|
|
203
201
|
`cloudformation deploy fails minutes from now, after the image build: ` +
|
|
204
202
|
`https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/agentcore-regions.html`);
|
|
205
203
|
}
|
|
206
|
-
// 3d.
|
|
204
|
+
// 3d. The role the runtime needs in this account, made if a first deploy finds it missing: a stop here costs nothing,
|
|
205
|
+
// and the same fault found by CloudFormation costs the build.
|
|
206
|
+
const roleGate = await ensureServiceLinkedRole(cli, RUNTIME_IDENTITY_ROLE, log);
|
|
207
|
+
if (roleGate !== undefined)
|
|
208
|
+
return gate(roleGate);
|
|
209
|
+
// 3e. Read the stack here, for the two questions that both depend on it: does this deploy destroy the agent's
|
|
207
210
|
// memory (below), and is there a failed first create to clear (step 7)? The destructive one is only worth saying
|
|
208
211
|
// while the multi-minute build has not run yet, and step 7 reuses this answer unless it was still in flight.
|
|
209
212
|
// An unanswered question must not read as "a first deploy, nothing to lose": `sts get-caller-identity` succeeding
|
|
@@ -236,6 +239,18 @@ probe = {}) {
|
|
|
236
239
|
`sessions, channel state and pending wake-ups start blank. ` +
|
|
237
240
|
`Cross-deploy memory needs a real volume: \`deploy fly\` or \`deploy railway\`.`);
|
|
238
241
|
}
|
|
242
|
+
// 3f. The deployment's values, after the only questions that precede a write and before the build: the secret the stack reads, made to hold every name the definition
|
|
243
|
+
// requires (a coding agent's run, with nobody to ask, stops here naming the commands).
|
|
244
|
+
const secretStore = agentcoreSecretStore(aws, agentcoreSecretName(plan.name), log);
|
|
245
|
+
try {
|
|
246
|
+
await secretStore.ensure();
|
|
247
|
+
}
|
|
248
|
+
catch (error) {
|
|
249
|
+
return gate(error instanceof Error ? error.message : String(error));
|
|
250
|
+
}
|
|
251
|
+
const secretsGate = await plan.provideSecrets(secretStore);
|
|
252
|
+
if (secretsGate !== undefined)
|
|
253
|
+
return gate(secretsGate);
|
|
239
254
|
// 4.
|
|
240
255
|
const registry = `${account}.dkr.ecr.${region}.amazonaws.com`;
|
|
241
256
|
const image = `${registry}/${repo}:${plan.tag}`;
|
|
@@ -306,7 +321,7 @@ probe = {}) {
|
|
|
306
321
|
if ((await docker(buildArgs)).code !== 0) {
|
|
307
322
|
return gate("`docker buildx build` failed — see the output above; fix and re-run");
|
|
308
323
|
}
|
|
309
|
-
// 7. The
|
|
324
|
+
// 7. The 3e answer stands unless it was still settling then — a second read is the narrow exception, not the rule.
|
|
310
325
|
const beforeDeploy = settling ? await readStackStatus() : stackStatus;
|
|
311
326
|
if ("ok" in beforeDeploy && beforeDeploy.ok === "ROLLBACK_COMPLETE") {
|
|
312
327
|
log(`stack ${stack} is ROLLBACK_COMPLETE (a failed first create) — deleting it before re-creating…`);
|
|
@@ -318,7 +333,14 @@ probe = {}) {
|
|
|
318
333
|
}
|
|
319
334
|
}
|
|
320
335
|
log(`deploying stack ${stack}…`);
|
|
321
|
-
|
|
336
|
+
let secretArn;
|
|
337
|
+
try {
|
|
338
|
+
secretArn = await secretStore.arn();
|
|
339
|
+
}
|
|
340
|
+
catch (error) {
|
|
341
|
+
return gate(error instanceof Error ? error.message : String(error));
|
|
342
|
+
}
|
|
343
|
+
const paramsPath = await writeSecretFile(paramsFileContent(image, secretArn, plan, forwarderParams));
|
|
322
344
|
const deployed = await aws([
|
|
323
345
|
"cloudformation",
|
|
324
346
|
"deploy",
|
|
@@ -386,7 +408,7 @@ probe = {}) {
|
|
|
386
408
|
// Verify storage initialization and channel construction before registering webhooks.
|
|
387
409
|
if (url) {
|
|
388
410
|
log("probing the deployed runtime (storage initialization + channel construction)…");
|
|
389
|
-
const verdict = await probeRuntime(`${url}${RESERVED_PATHS.probe}`, plan.
|
|
411
|
+
const verdict = await probeRuntime(`${url}${RESERVED_PATHS.probe}`, plan.ingressSecret, probe.fetchImpl ?? fetch, probe.timeoutMs, probe.intervalMs);
|
|
390
412
|
if (!verdict.ok)
|
|
391
413
|
return gate(verdict.gate);
|
|
392
414
|
log("runtime verified (storage ready, channels constructed)");
|
|
@@ -397,22 +419,23 @@ probe = {}) {
|
|
|
397
419
|
notLoggedIn,
|
|
398
420
|
channels: plan.channels,
|
|
399
421
|
log,
|
|
400
|
-
// The
|
|
401
|
-
|
|
402
|
-
? { afterLogin: "they work again: the forwarder URL is unchanged, so nothing needs pointing" }
|
|
403
|
-
: { baseUrl: url, afterLogin: "point the channels by hand (a redeploy would wipe that login again)" }),
|
|
422
|
+
// The runtime points its own channels at the forwarder URL as the probe wakes it, login or not.
|
|
423
|
+
afterLogin: "they work again: the runtime has pointed its own channels, so nothing needs pointing",
|
|
404
424
|
}));
|
|
405
425
|
}
|
|
406
|
-
// 9.
|
|
407
|
-
const registrationGateMsg =
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
:
|
|
426
|
+
// 9. The runtime points its own channels as the probe wakes it; this reads what it made of them.
|
|
427
|
+
const registrationGateMsg = await confirmWebhooks({
|
|
428
|
+
baseUrl: url,
|
|
429
|
+
publicUrl: plan.publicUrl ?? url,
|
|
430
|
+
channels: plan.channels,
|
|
431
|
+
registrars,
|
|
432
|
+
log,
|
|
433
|
+
...(plan.recordUrl === undefined ? {} : { record: plan.recordUrl }),
|
|
434
|
+
retryHint: "re-run to retry registration (steps already done are skipped)",
|
|
435
|
+
probe: forwarderWebhooks(`${url}${RESERVED_PATHS.probe}`, plan.ingressSecret, probe.fetchImpl ?? fetch),
|
|
436
|
+
intervalMs: probe.intervalMs ?? WEBHOOKS_INTERVAL_MS,
|
|
437
|
+
...(probe.timeoutMs === undefined ? {} : { timeoutMs: probe.timeoutMs }),
|
|
438
|
+
});
|
|
416
439
|
if (registrationGateMsg)
|
|
417
440
|
return gate(registrationGateMsg);
|
|
418
441
|
return { ok: true, runtimeArn, url };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** The secret's JSON object, every value a string. */
|
|
2
|
+
export declare function readSecretValues(secretArn: string): Promise<Record<string, string>>;
|
|
3
|
+
/**
|
|
4
|
+
* Put the secret's values into `env`, unless the platform set the name itself (the platform's variables win, as they do
|
|
5
|
+
* over a value file anywhere). Without `FASTAGENT_SECRET_ARN` there is nothing to read.
|
|
6
|
+
*/
|
|
7
|
+
export declare function applyRuntimeSecrets(env?: NodeJS.ProcessEnv): Promise<void>;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the AgentCore container does with its deployment's values: read the Secrets Manager secret `FASTAGENT_SECRET_ARN`
|
|
3
|
+
* names and put each name it holds into the environment (agent-artifact.md §6.2).
|
|
4
|
+
*
|
|
5
|
+
* The runtime has no way to reference a secret from an environment variable, so the container reads it itself, on its
|
|
6
|
+
* first invocation. Its credentials are the execution role's, from the microVM's instance-metadata service (IMDSv2); the
|
|
7
|
+
* request is SigV4-signed with the signer `shell.ts` already uses, so no AWS SDK is installed in the image.
|
|
8
|
+
*/
|
|
9
|
+
import { SignatureV4 } from "@smithy/signature-v4";
|
|
10
|
+
import { SECRET_ARN_ENV } from "./plan.js";
|
|
11
|
+
import { Sha256 } from "./sigv4.js";
|
|
12
|
+
const IMDS = "http://169.254.169.254/latest";
|
|
13
|
+
const TIMEOUT_MS = 5_000;
|
|
14
|
+
async function imdsText(url, init) {
|
|
15
|
+
const response = await fetch(url, { ...init, signal: AbortSignal.timeout(TIMEOUT_MS) });
|
|
16
|
+
if (!response.ok)
|
|
17
|
+
throw new Error(`${init.method ?? "GET"} ${url} answered ${response.status}`);
|
|
18
|
+
return response.text();
|
|
19
|
+
}
|
|
20
|
+
/** The execution role's credentials, as the metadata service hands them to the microVM. */
|
|
21
|
+
async function roleCredentials() {
|
|
22
|
+
const token = await imdsText(`${IMDS}/api/token`, {
|
|
23
|
+
method: "PUT",
|
|
24
|
+
headers: { "x-aws-ec2-metadata-token-ttl-seconds": "60" },
|
|
25
|
+
});
|
|
26
|
+
const headers = { "x-aws-ec2-metadata-token": token };
|
|
27
|
+
const role = (await imdsText(`${IMDS}/meta-data/iam/security-credentials/`, { headers })).trim();
|
|
28
|
+
const parsed = JSON.parse(await imdsText(`${IMDS}/meta-data/iam/security-credentials/${role}`, { headers }));
|
|
29
|
+
const { AccessKeyId, SecretAccessKey, Token } = (parsed ?? {});
|
|
30
|
+
if (typeof AccessKeyId !== "string" || typeof SecretAccessKey !== "string" || typeof Token !== "string") {
|
|
31
|
+
throw new Error(`the metadata service's credentials for ${role} are not an access key, a secret and a token`);
|
|
32
|
+
}
|
|
33
|
+
return { accessKeyId: AccessKeyId, secretAccessKey: SecretAccessKey, sessionToken: Token };
|
|
34
|
+
}
|
|
35
|
+
/** `arn:<partition>:secretsmanager:<region>:<account>:secret:<name>-<suffix>` → its region. */
|
|
36
|
+
function regionOf(secretArn) {
|
|
37
|
+
const [, , service, region] = secretArn.split(":");
|
|
38
|
+
if (service !== "secretsmanager" || !region)
|
|
39
|
+
throw new Error(`${SECRET_ARN_ENV} is not a Secrets Manager ARN`);
|
|
40
|
+
return region;
|
|
41
|
+
}
|
|
42
|
+
/** The secret's JSON object, every value a string. */
|
|
43
|
+
export async function readSecretValues(secretArn) {
|
|
44
|
+
const region = regionOf(secretArn);
|
|
45
|
+
const hostname = `secretsmanager.${region}.amazonaws.com`;
|
|
46
|
+
const body = JSON.stringify({ SecretId: secretArn });
|
|
47
|
+
const signed = await new SignatureV4({
|
|
48
|
+
service: "secretsmanager",
|
|
49
|
+
region,
|
|
50
|
+
credentials: await roleCredentials(),
|
|
51
|
+
sha256: Sha256,
|
|
52
|
+
}).sign({
|
|
53
|
+
method: "POST",
|
|
54
|
+
protocol: "https:",
|
|
55
|
+
hostname,
|
|
56
|
+
path: "/",
|
|
57
|
+
body,
|
|
58
|
+
headers: {
|
|
59
|
+
host: hostname,
|
|
60
|
+
"content-type": "application/x-amz-json-1.1",
|
|
61
|
+
"x-amz-target": "secretsmanager.GetSecretValue",
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
const response = await fetch(`https://${hostname}/`, {
|
|
65
|
+
method: "POST",
|
|
66
|
+
headers: signed.headers,
|
|
67
|
+
body,
|
|
68
|
+
signal: AbortSignal.timeout(TIMEOUT_MS),
|
|
69
|
+
});
|
|
70
|
+
const answer = await response.text();
|
|
71
|
+
// Never the parser's message: it quotes the text around the error, and this document holds every value.
|
|
72
|
+
if (!response.ok)
|
|
73
|
+
throw new Error(`GetSecretValue answered ${response.status}: ${answer.slice(0, 200)}`);
|
|
74
|
+
let values;
|
|
75
|
+
try {
|
|
76
|
+
values = JSON.parse(JSON.parse(answer).SecretString ?? "");
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
throw new Error(`the secret ${secretArn} is not a JSON object of values`);
|
|
80
|
+
}
|
|
81
|
+
if (values === null || typeof values !== "object" || Array.isArray(values)) {
|
|
82
|
+
throw new Error(`the secret ${secretArn} is not a JSON object of values`);
|
|
83
|
+
}
|
|
84
|
+
for (const [name, value] of Object.entries(values)) {
|
|
85
|
+
if (typeof value !== "string")
|
|
86
|
+
throw new Error(`the secret ${secretArn}: ${name} is not a string`);
|
|
87
|
+
}
|
|
88
|
+
return values;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Put the secret's values into `env`, unless the platform set the name itself (the platform's variables win, as they do
|
|
92
|
+
* over a value file anywhere). Without `FASTAGENT_SECRET_ARN` there is nothing to read.
|
|
93
|
+
*/
|
|
94
|
+
export async function applyRuntimeSecrets(env = process.env) {
|
|
95
|
+
const arn = env[SECRET_ARN_ENV];
|
|
96
|
+
if (!arn)
|
|
97
|
+
return;
|
|
98
|
+
let values;
|
|
99
|
+
try {
|
|
100
|
+
values = await readSecretValues(arn);
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
throw new Error(`could not read the deployment's values from ${arn}: ${error instanceof Error ? error.message : String(error)}`);
|
|
104
|
+
}
|
|
105
|
+
for (const [name, value] of Object.entries(values))
|
|
106
|
+
if (!(name in env))
|
|
107
|
+
env[name] = value;
|
|
108
|
+
}
|
|
@@ -13,14 +13,15 @@
|
|
|
13
13
|
* terminal, so the command switches it to raw, no echo (answers are not echoed back, and a long line is not cut by
|
|
14
14
|
* the terminal's line editor) before handing it to `exec`.
|
|
15
15
|
*/
|
|
16
|
-
import {
|
|
16
|
+
import { randomUUID } from "node:crypto";
|
|
17
17
|
import { mkdtemp, readFile, rm } from "node:fs/promises";
|
|
18
18
|
import { tmpdir } from "node:os";
|
|
19
19
|
import { join } from "node:path";
|
|
20
20
|
import { PassThrough, Writable } from "node:stream";
|
|
21
21
|
import { SignatureV4 } from "@smithy/signature-v4";
|
|
22
|
-
import {
|
|
22
|
+
import { RuntimeWebSocket, pinger } from "../../runtime-websocket.js";
|
|
23
23
|
import { awsCli, awsJson } from "./aws-cli.js";
|
|
24
|
+
import { Sha256 } from "./sigv4.js";
|
|
24
25
|
import { MOUNT, PROBE_PAYLOAD } from "./plan.js";
|
|
25
26
|
const STDIN = 0x00;
|
|
26
27
|
const STDOUT = 0x01;
|
|
@@ -29,6 +30,7 @@ const STATUS = 0x03;
|
|
|
29
30
|
const HEARTBEAT = 0x05;
|
|
30
31
|
/** The service closes a connection whose frame is larger (code 1009). */
|
|
31
32
|
const MAX_PAYLOAD = 64 * 1024 - 1;
|
|
33
|
+
const WS_OPEN = 1;
|
|
32
34
|
/** The SDK's interval; a connection with no ping for ~60s is dropped by the service. */
|
|
33
35
|
const KEEPALIVE_MS = 30_000;
|
|
34
36
|
/** The longest a presigned shell URL may live (the SDK refuses more). */
|
|
@@ -49,20 +51,6 @@ async function awsCliCredentials(aws) {
|
|
|
49
51
|
return read.ok;
|
|
50
52
|
throw new Error(`could not read AWS credentials from the AWS CLI (${"absent" in read ? "none configured" : read.unreadable})`);
|
|
51
53
|
}
|
|
52
|
-
/** SHA-256 / HMAC-SHA-256 over node:crypto, in the shape SigV4's signer takes. */
|
|
53
|
-
class Sha256 {
|
|
54
|
-
hash;
|
|
55
|
-
constructor(secret) {
|
|
56
|
-
const key = secret instanceof ArrayBuffer ? Buffer.from(secret) : secret;
|
|
57
|
-
this.hash = key === undefined ? createHash("sha256") : createHmac("sha256", key);
|
|
58
|
-
}
|
|
59
|
-
update(data) {
|
|
60
|
-
this.hash.update(data instanceof ArrayBuffer ? Buffer.from(data) : data);
|
|
61
|
-
}
|
|
62
|
-
async digest() {
|
|
63
|
-
return new Uint8Array(this.hash.digest());
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
54
|
/** The runtime's region, from its ARN (`arn:<partition>:bedrock-agentcore:<region>:<account>:runtime/<id>`). */
|
|
67
55
|
function regionOf(runtimeArn) {
|
|
68
56
|
const region = runtimeArn.split(":")[3];
|
|
@@ -105,7 +93,12 @@ export function shellExit(status) {
|
|
|
105
93
|
/** Open `command` in a new shell on `url`: stdin/stdout as streams, the other channels folded in. */
|
|
106
94
|
export function openShellChannel(url, command) {
|
|
107
95
|
const output = new PassThrough();
|
|
108
|
-
const ws = new
|
|
96
|
+
const ws = new RuntimeWebSocket(url);
|
|
97
|
+
const ping = pinger(ws);
|
|
98
|
+
if (ping === undefined) {
|
|
99
|
+
ws.close();
|
|
100
|
+
throw new Error("this runtime cannot send WebSocket pings, and the AgentCore service drops a shell connection that has none for about a minute");
|
|
101
|
+
}
|
|
109
102
|
ws.binaryType = "arraybuffer";
|
|
110
103
|
const frame = (channel, payload = Buffer.alloc(0)) => ws.send(Buffer.concat([Buffer.from([channel]), payload]));
|
|
111
104
|
const stdin = (bytes) => {
|
|
@@ -122,7 +115,7 @@ export function openShellChannel(url, command) {
|
|
|
122
115
|
ws.addEventListener("open", () => {
|
|
123
116
|
stdin(Buffer.from(`stty raw -echo 2>/dev/null; exec sh -c '${command}'\n`));
|
|
124
117
|
keepalive = setInterval(() => {
|
|
125
|
-
ping(
|
|
118
|
+
ping();
|
|
126
119
|
frame(HEARTBEAT);
|
|
127
120
|
}, KEEPALIVE_MS);
|
|
128
121
|
});
|
|
@@ -157,7 +150,7 @@ export function openShellChannel(url, command) {
|
|
|
157
150
|
const input = new Writable({
|
|
158
151
|
write(chunk, _encoding, done) {
|
|
159
152
|
void opened.then((open) => {
|
|
160
|
-
if (open && ws.readyState ===
|
|
153
|
+
if (open && ws.readyState === WS_OPEN)
|
|
161
154
|
stdin(chunk);
|
|
162
155
|
done();
|
|
163
156
|
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** SHA-256 / HMAC-SHA-256 over node:crypto, in the shape SigV4's signer takes. */
|
|
2
|
+
export declare class Sha256 {
|
|
3
|
+
private readonly hash;
|
|
4
|
+
constructor(secret?: string | ArrayBuffer | ArrayBufferView);
|
|
5
|
+
update(data: string | ArrayBuffer | ArrayBufferView): void;
|
|
6
|
+
digest(): Promise<Uint8Array>;
|
|
7
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { createHash, createHmac } from "node:crypto";
|
|
2
|
+
/** SHA-256 / HMAC-SHA-256 over node:crypto, in the shape SigV4's signer takes. */
|
|
3
|
+
export class Sha256 {
|
|
4
|
+
hash;
|
|
5
|
+
constructor(secret) {
|
|
6
|
+
const key = secret instanceof ArrayBuffer ? Buffer.from(secret) : secret;
|
|
7
|
+
this.hash = key === undefined ? createHash("sha256") : createHmac("sha256", key);
|
|
8
|
+
}
|
|
9
|
+
update(data) {
|
|
10
|
+
this.hash.update(data instanceof ArrayBuffer ? Buffer.from(data) : data);
|
|
11
|
+
}
|
|
12
|
+
async digest() {
|
|
13
|
+
return new Uint8Array(this.hash.digest());
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The service-linked role AgentCore's runtime needs in the account, checked before a deploy builds anything. Without it
|
|
3
|
+
* CloudFormation fails at the runtime, minutes after the image push, with a bare `AccessDenied` that names nothing.
|
|
4
|
+
*
|
|
5
|
+
* Read first, create only when it is absent: IAM authorizes `CreateServiceLinkedRole` before it says a role already
|
|
6
|
+
* exists, so creating unconditionally would refuse a deployer who lacks that permission in every account where the role
|
|
7
|
+
* is long since there. A read that cannot be answered warns and goes on, as the region probe does.
|
|
8
|
+
*/
|
|
9
|
+
import type { AwsCli } from "./aws-cli.ts";
|
|
10
|
+
export interface ServiceLinkedRole {
|
|
11
|
+
/** The service principal the role is created for. */
|
|
12
|
+
service: string;
|
|
13
|
+
/** The role's name, which is what `iam get-role` takes. */
|
|
14
|
+
name: string;
|
|
15
|
+
}
|
|
16
|
+
export declare const RUNTIME_IDENTITY_ROLE: ServiceLinkedRole;
|
|
17
|
+
/** A gate line when the role is absent and cannot be made, else undefined. */
|
|
18
|
+
export declare function ensureServiceLinkedRole(cli: AwsCli, role: ServiceLinkedRole, log: (message: string) => void): Promise<string | undefined>;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export const RUNTIME_IDENTITY_ROLE = {
|
|
2
|
+
service: "runtime-identity.bedrock-agentcore.amazonaws.com",
|
|
3
|
+
name: "AWSServiceRoleForBedrockAgentCoreRuntimeIdentity",
|
|
4
|
+
};
|
|
5
|
+
/** A gate line when the role is absent and cannot be made, else undefined. */
|
|
6
|
+
export async function ensureServiceLinkedRole(cli, role, log) {
|
|
7
|
+
const found = await cli.present(["iam", "get-role", "--role-name", role.name]);
|
|
8
|
+
if ("ok" in found)
|
|
9
|
+
return undefined;
|
|
10
|
+
if ("unreadable" in found) {
|
|
11
|
+
log(`warn: could not check for the service-linked role ${role.name} (${found.unreadable.split("\n")[0]}) — if the ` +
|
|
12
|
+
`account has never used AgentCore and this principal may not create it, cloudformation deploy fails at the ` +
|
|
13
|
+
`runtime, after the image build`);
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
log(`creating the service-linked role ${role.name}…`);
|
|
17
|
+
const created = await cli.write(["iam", "create-service-linked-role", "--aws-service-name", role.service]);
|
|
18
|
+
if ("done" in created)
|
|
19
|
+
return undefined;
|
|
20
|
+
// Made by another deploy between the read and the write: it is there, which is all that is wanted.
|
|
21
|
+
if ("refused" in created && /has been taken/i.test(created.refused))
|
|
22
|
+
return undefined;
|
|
23
|
+
const why = "refused" in created ? created.refused : "it was not found";
|
|
24
|
+
return (`the service-linked role ${role.name} that AgentCore's runtime needs does not exist in this account, and it could ` +
|
|
25
|
+
`not be created (${why}) — have someone with iam:CreateServiceLinkedRole run ` +
|
|
26
|
+
`\`aws iam create-service-linked-role --aws-service-name ${role.service}\`, then re-run`);
|
|
27
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { CliRunner } from "../runner.ts";
|
|
2
|
+
import type { SecretStore } from "../secret-store.ts";
|
|
3
|
+
export interface AgentcoreSecretStore extends SecretStore {
|
|
4
|
+
/** The secret's ARN, which the stack takes as its `SecretArn` parameter. */
|
|
5
|
+
arn(): Promise<string>;
|
|
6
|
+
}
|
|
7
|
+
export declare function agentcoreSecretStore(aws: CliRunner, secretName: string, log: (message: string) => void): AgentcoreSecretStore;
|