Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/workflows/verify.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ jobs:
with:
ref: ${{ inputs.ref }}
persist-credentials: false
# Native binaries still run the project's CDK app in Node. CodeBuild's
# Windows image ships an older Node; use the supported runtime for tests.
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: "22"
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
- run: bun install --frozen-lockfile

Expand Down Expand Up @@ -89,6 +94,9 @@ jobs:
with:
ref: ${{ inputs.ref }}
persist-credentials: false
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: "22"
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
- run: bun install --frozen-lockfile
- run: bun run build
Expand Down
59 changes: 59 additions & 0 deletions docs/runtime-prebuilt-images.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Deploy an existing Runtime image

Use `--advanced-configuration` on `agentcore add runtime` to configure an image you have already
built and pushed to ECR:

```sh
agentcore add runtime --name agent \
--advanced-configuration '{"containerUri":"123456789012.dkr.ecr.us-west-2.amazonaws.com/agent:v1"}'
agentcore deploy
agentcore invoke --runtime agent --payload '{"prompt":"Hello"}'
```

Private ECR tags and digests, and public ECR image URIs, are supported. The image must satisfy the
AgentCore Runtime contract, including Linux ARM64 and the selected protocol's server requirements.
The toolkit does not build, copy, publish, or check the existence of this image.

The flag accepts one JSON object. Keys use the same camelCase names and values as `agentcore.json`:

- `containerUri`, `description`, `executionRoleArn`, `additionalPolicies`
- `envVars` (an array of `{ "name": "...", "value": "..." }` objects)
- `networkMode`, `networkConfig`
- `authorizerType`, `authorizerConfiguration`
- `protocol`, `requestHeaderAllowlist`
- `lifecycleConfiguration`, `filesystemConfigurations`, `tags`

Unknown keys, invalid values, and a setting supplied both in this object and through an existing
standalone flag are rejected before the project is changed. Existing standalone flags continue to
work. Basic options such as `--name`, `--language`, and Bedrock Agent import selectors stay outside
this object. There is no global deploy override.

An existing image does not need `--language`. The persisted Runtime still has `build: "Container"`
and schema-required `entrypoint` and `codeLocation` placeholders. The CLI creates no source folder,
and build/deploy do not read those placeholders or any local Dockerfile or manifest.

`agentcore build` synthesizes the deployment. `agentcore deploy` uses the configured URI directly
and grants the Runtime execution role the applicable ECR pull permissions. Neither command
creates a managed ECR repository, CodeBuild project, source archive, or image-build custom resource
for this Runtime. `agentcore dev` requires a Runtime with local source; use deploy/invoke for an
existing image.

Change `containerUri` in `agentcore/agentcore.json` and deploy to update the image. Use a new tag or
digest to trigger an update; replacing the contents of an unchanged tag does not change the
CloudFormation template. Named endpoints continue to select Runtime versions.

`networkMode: "VPC"` configures the deployed workload. A pre-built image does not require
`networkConfig.vpcId` for CodeBuild and is not subject to CodeBuild's five-security-group check.
Runtime networking must still satisfy VPC and account limits, including the security-groups-per-ENI
quota. This setting does not restrict inbound invocation to private endpoints.
The deployed workload still needs the [documented VPC networking prerequisites](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/agentcore-vpc.html#agentcore-vpc-endpoints),
including ECR and S3 connectivity for container images. Skipping CodeBuild removes the remote
package-installation step; it does not remove the Runtime's image-pull networking requirements.

This feature requires a released `@aws/agentcore-cdk` package containing CDK PR #440. Older packages
can silently strip unknown schema properties; the CLI now detects that condition and fails before
deployment instead of accidentally choosing CodeBuild. Release the CDK change and update the
vended package pin before releasing the CLI change.

The Dockerfile/CodeBuild deployment path remains the default. Toolkit-managed local builds and
private-only invocation are separate proposals.
19 changes: 19 additions & 0 deletions e2eTest/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,22 @@ run through a shell, so paths containing spaces, quoting, and shell syntax are n
```sh
AGENTCORE_CLI_PATH=/path/to/agentcore bun run test:e2e
```

The pre-built Runtime tests accept existing Linux ARM64 HTTP images. The test handler must return
`{"version":"v1","echo":<request>}` for the initial image and `"v2"` for the update:

```sh
export AGENTCORE_PREBUILT_V1_URI='<private ECR tag URI>'
export AGENTCORE_PREBUILT_V1_DIGEST_URI='<same image by digest>'
export AGENTCORE_PREBUILT_V2_URI='<updated private ECR image>'
# Optional: an existing public ECR image with the v1 handler.
export AGENTCORE_PREBUILT_PUBLIC_URI='<public ECR image URI>'
# Before CDK PR #440 is released, install a locally packed feature package.
export AGENTCORE_CDK_PACKAGE=/absolute/path/to/aws-agentcore-cdk.tgz
bunx vitest run --config vitest.prebuilt.e2e.config.ts
```

These tests skip when their image variables are absent. They exercise the JSON advanced flag,
build/deploy/invoke, digest pinning, a named endpoint, an image update retaining the Runtime ID,
absence of build resources, and stack teardown. They never create or publish the image fixtures.
The focused configuration cleans up only the stacks this suite creates.
202 changes: 202 additions & 0 deletions e2eTest/project/prebuilt-runtime.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
import { describe, expect, test } from "vitest";
import { mkdtemp, readFile, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { execFile } from "node:child_process";
import { promisify } from "node:util";
import { randomUUID } from "node:crypto";
import {
CloudFormationClient,
DescribeStacksCommand,
GetTemplateCommand,
} from "@aws-sdk/client-cloudformation";
import z from "zod";
import { TAGS } from "../constants";
import { CliRunner, parseResult } from "../helpers/run";
import { retry } from "../helpers/retry";
import { TIMEOUT_MS } from "../timeouts";

// Supply existing ARM64 HTTP fixtures; this suite never creates or publishes images.
const v1 = process.env.AGENTCORE_PREBUILT_V1_URI;
const digest = process.env.AGENTCORE_PREBUILT_V1_DIGEST_URI;
const v2 = process.env.AGENTCORE_PREBUILT_V2_URI;
const publicUri = process.env.AGENTCORE_PREBUILT_PUBLIC_URI;
const resultSchema = z.object({ message: z.string() }).passthrough();
const operationSchema = z.object({ operation: z.string() });
const invocationSchema = z.object({
statusCode: z.literal(200),
body: z.string(),
complete: z.literal(true),
});

describe("pre-built Runtime images", { tags: [TAGS.RUNTIME] }, () => {
const cli = new CliRunner();
const cfn = new CloudFormationClient({
region: process.env.AWS_REGION ?? "us-west-2",
});

async function createProject() {
const parent = await mkdtemp(join(tmpdir(), "agentcore-prebuilt-e2e-"));
const name = `pbe2e${randomUUID().replaceAll("-", "").slice(0, 12)}`;
const created = parseResult(
z.object({ project: z.object({ path: z.string() }) }),
await cli.run(
["create", "--name", name, "--template", "empty", "--skip-git", "--json"],
parent,
),
);
const path = created.project.path;
// Before the CDK release, exercise the actual package built from PR #440.
if (process.env.AGENTCORE_CDK_PACKAGE) {
await promisify(execFile)(
process.platform === "win32" ? "npm.cmd" : "npm",
["install", process.env.AGENTCORE_CDK_PACKAGE],
{ cwd: join(path, "agentcore", "cdk") },
);
}
const stack = `AgentCore-${name}-default`;
console.info(`pre-built Runtime test project: ${path}; stack: ${stack}`);
return { path, stack };
}

async function addImage(path: string, name: string, containerUri: string) {
parseResult(
operationSchema,
await cli.run(
[
"add",
"runtime",
"--name",
name,
"--advanced-configuration",
JSON.stringify({ containerUri }),
"--json",
],
path,
),
);
await expect(readFile(join(path, "app", name, "main.py"))).rejects.toMatchObject({
code: "ENOENT",
});
}

async function invoke(path: string, name: string, version: string, qualifier?: string) {
await retry(
async () => {
const response = parseResult(
invocationSchema,
await cli.run(
[
"invoke",
"--runtime",
name,
"--session-id",
randomUUID(),
"--payload",
'{"test":"prebuilt-e2e"}',
"--json",
...(qualifier ? ["--qualifier", qualifier] : []),
],
path,
),
);
expect(JSON.parse(response.body)).toEqual({
version,
echo: { test: "prebuilt-e2e" },
});
},
TIMEOUT_MS.PROJECT_INVOKE,
2_000,
);
}

async function runtimeIds(stack: string) {
const response = await cfn.send(new DescribeStacksCommand({ StackName: stack }));
return Object.fromEntries(
(response.Stacks?.[0]?.Outputs ?? [])
.filter((output) => output.ExportName?.endsWith("-RuntimeId"))
.map((output) => [output.ExportName!, output.OutputValue!]),
);
}

async function assertNoBuilder(stack: string) {
const response = await cfn.send(new GetTemplateCommand({ StackName: stack }));
const template = JSON.parse(response.TemplateBody!) as {
Resources: Record<string, { Type: string }>;
};
const types = Object.values(template.Resources).map((resource) => resource.Type);
expect(types).not.toContain("AWS::CodeBuild::Project");
expect(types).not.toContain("AWS::ECR::Repository");
expect(types.every((type) => !type.startsWith("Custom::"))).toBe(true);
}

async function cleanup(path: string) {
parseResult(operationSchema, await cli.run(["remove", "all", "--yes", "--json"], path));
parseResult(resultSchema, await cli.run(["deploy", "--yes", "--json"], path));
}

test.skipIf(!v1 || !digest || !v2)(
"tag, digest, named endpoint, image update, and stack teardown",
{ timeout: 5 * TIMEOUT_MS.PROJECT_DEPLOY + 5 * TIMEOUT_MS.PROJECT_INVOKE },
async () => {
const { path, stack } = await createProject();
try {
await addImage(path, "tagged", v1!);
await addImage(path, "digest", digest!);
parseResult(resultSchema, await cli.run(["build", "--json"], path));
parseResult(resultSchema, await cli.run(["deploy", "--yes", "--json"], path));
await assertNoBuilder(stack);
await invoke(path, "tagged", "v1");
await invoke(path, "digest", "v1");
const idsBefore = await runtimeIds(stack);

parseResult(
operationSchema,
await cli.run(
[
"add",
"runtime-endpoint",
"--runtime",
"tagged",
"--name",
"stable",
"--version",
"1",
"--json",
],
path,
),
);
const specPath = join(path, "agentcore", "agentcore.json");
const spec = JSON.parse(await readFile(specPath, "utf8"));
spec.runtimes.find((runtime: { name: string }) => runtime.name === "tagged").containerUri =
v2;
await writeFile(specPath, JSON.stringify(spec, null, 2));
parseResult(resultSchema, await cli.run(["deploy", "--yes", "--json"], path));
expect(await runtimeIds(stack)).toEqual(idsBefore);
await assertNoBuilder(stack);
await invoke(path, "tagged", "v2");
await invoke(path, "digest", "v1");
await invoke(path, "tagged", "v1", "stable");
} finally {
await cleanup(path);
}
},
);

test.skipIf(!publicUri)(
"an existing public ECR image deploys and invokes without a build",
{ timeout: 2 * TIMEOUT_MS.PROJECT_DEPLOY + TIMEOUT_MS.PROJECT_INVOKE },
async () => {
const { path, stack } = await createProject();
try {
await addImage(path, "public", publicUri!);
parseResult(resultSchema, await cli.run(["deploy", "--yes", "--json"], path));
await assertNoBuilder(stack);
await invoke(path, "public", "v1");
} finally {
await cleanup(path);
}
},
);
});
3 changes: 2 additions & 1 deletion src/core/project/backends/cdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import type {
ResolvedDeployedResource,
ResolvedProjectResource,
} from "../../../handlers/project/types";
import { cdkCompatibilityWarning } from "./cdk/compatibility";
import { assertPrebuiltRuntimeSupport, cdkCompatibilityWarning } from "./cdk/compatibility";
import {
createLineSplitter,
FsReadWriteJson,
Expand Down Expand Up @@ -208,6 +208,7 @@ export class CdkBackend implements ProjectBackend {
);
}
await this.checkTool("npm", "Install Node.js: https://nodejs.org/");
await assertPrebuiltRuntimeSupport(cdkDir, project.spec.runtimes, this.runner);
}

private async *synthesize(
Expand Down
Loading
Loading