From e44ddc17bf6f24e41f29f2b9daddd05766fcc25a Mon Sep 17 00:00:00 2001 From: Hweinstock Date: Fri, 28 Aug 2026 16:48:25 +0000 Subject: [PATCH 1/2] feat(templates): support Container builds in the strands-http-python template --- .../strands-http-python/Dockerfile.template | 40 +++++++++++++++++ .../strands-http-python/dockerignore.template | 27 +++++++++++ src/core/project/templates/runtime.ts | 12 +++-- .../project/add/runtime/index.test.ts | 16 +++++-- src/handlers/project/project.test.ts | 45 +++++++++++++++++++ 5 files changed, 132 insertions(+), 8 deletions(-) create mode 100644 src/assets/templates/strands-http-python/Dockerfile.template create mode 100644 src/assets/templates/strands-http-python/dockerignore.template diff --git a/src/assets/templates/strands-http-python/Dockerfile.template b/src/assets/templates/strands-http-python/Dockerfile.template new file mode 100644 index 000000000..cb3569eff --- /dev/null +++ b/src/assets/templates/strands-http-python/Dockerfile.template @@ -0,0 +1,40 @@ +FROM public.ecr.aws/docker/library/python:3.12-slim-trixie + +RUN pip install --no-cache-dir uv + +ARG UV_DEFAULT_INDEX +ARG UV_INDEX + +WORKDIR /app + +ENV UV_SYSTEM_PYTHON=1 \ + UV_COMPILE_BYTECODE=1 \ + UV_NO_PROGRESS=1 \ + PYTHONUNBUFFERED=1 \ + DOCKER_CONTAINER=1 \ + UV_DEFAULT_INDEX=${UV_DEFAULT_INDEX} \ + UV_INDEX=${UV_INDEX} \ + PATH="/app/.venv/bin:$PATH" + +RUN useradd -m -u 1000 bedrock_agentcore + +COPY pyproject.toml uv.lock ./ +RUN uv sync --frozen --no-dev --no-install-project + +COPY --chown=bedrock_agentcore:bedrock_agentcore . . +RUN uv sync --frozen --no-dev + +USER bedrock_agentcore + +# AgentCore Runtime service contract ports +# https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/runtime-service-contract.html +# 8080: HTTP Mode +# 8000: MCP Mode +# 9000: A2A Mode +EXPOSE 8080 8000 9000 + +{{#if enableOtel}} +CMD ["opentelemetry-instrument", "python", "-m", "{{entrypoint}}"] +{{else}} +CMD ["python", "-m", "{{entrypoint}}"] +{{/if}} diff --git a/src/assets/templates/strands-http-python/dockerignore.template b/src/assets/templates/strands-http-python/dockerignore.template new file mode 100644 index 000000000..a0c4eb658 --- /dev/null +++ b/src/assets/templates/strands-http-python/dockerignore.template @@ -0,0 +1,27 @@ +# Python +__pycache__/ +*.py[cod] +*.egg-info/ +.venv/ +dist/ +build/ + +# IDE +.vscode/ +.idea/ + +# Testing +.pytest_cache/ +.coverage +htmlcov/ + +# Secrets and environment files +.env +.env.* + +# Version control +.git/ + +# AgentCore build artifacts +.agentcore/artifacts/ +*.zip diff --git a/src/core/project/templates/runtime.ts b/src/core/project/templates/runtime.ts index 6a15566d1..a3e40fe4b 100644 --- a/src/core/project/templates/runtime.ts +++ b/src/core/project/templates/runtime.ts @@ -78,9 +78,6 @@ const getTemplateResolvers = (assetSource: AssetSource, templateRenderer: Templa if (input.protocol !== undefined && input.protocol !== "HTTP") throw new InputValidationError("the strands-python template only supports HTTP"); - if (input.scaffoldRuntimeInput.build !== "CodeZip") - throw new InputValidationError("the strands template only supports CodeZip builds"); - const filesystemConfigurations = input.filesystemConfigurations ?? []; const sessionStorageMountPath = filesystemConfigurations.flatMap((configuration) => "sessionStorage" in configuration ? [configuration.sessionStorage.mountPath] : [], @@ -115,14 +112,21 @@ const getTemplateResolvers = (assetSource: AssetSource, templateRenderer: Templa s3Mounts, needsOs: filesystemConfigurations.length > 0, hasConfigBundle: false, + enableOtel: true, + entrypoint: input.scaffoldRuntimeInput.entrypoint.replace(/\.py$/, ""), }; + const isContainer = input.scaffoldRuntimeInput.build === "Container"; const tree = await FsTreeNode.fromAssetSource( { assetSource }, { assetDir: "templates/strands-http-python" }, { rootDirName: input.name, transformContent: (raw) => templateRenderer.render(raw, context), - filter: (name, isDir) => memory !== undefined || !isDir || name !== "memory", + filter: (name, isDir) => { + if (isDir && name === "memory") return memory !== undefined; + if (name === "Dockerfile" || name === ".dockerignore") return isContainer; + return true; + }, }, ); return { diff --git a/src/handlers/project/add/runtime/index.test.ts b/src/handlers/project/add/runtime/index.test.ts index e5c2f047f..59d52a83a 100644 --- a/src/handlers/project/add/runtime/index.test.ts +++ b/src/handlers/project/add/runtime/index.test.ts @@ -101,6 +101,10 @@ describe("project add runtime", () => { "container template build override to CodeZip": { build: "CodeZip", }, + "strands template overrides to Container": { + build: "Container", + dockerfile: "Dockerfile", + }, "all infrastructure flags": { description: "Configured runtime", executionRoleArn: "arn:aws:iam::123456789012:role/MyRole", @@ -155,6 +159,10 @@ describe("project add runtime", () => { "container template build override to CodeZip", ["--name", "my_agent", "--template", "hello-world-python-container", "--build", "CodeZip"], ], + [ + "strands template overrides to Container", + ["--name", "my_agent", "--template", "strands-python", "--build", "Container"], + ], ["custom — all scaffolding flags", ["--name", "my_agent", ...allScaffoldingFlags]], [ "custom — framework strands", @@ -319,6 +327,10 @@ describe("project add runtime", () => { ? flags[buildFlagIndex + 1] === "Container" : flags.includes("hello-world-python-container"); expect(runtime.runtimeVersion).toBe(isContainer ? undefined : "PYTHON_3_14"); + expect(await Bun.file(join(projectRoot, "app", name, "Dockerfile")).exists()).toBe(isContainer); + expect(await Bun.file(join(projectRoot, "app", name, ".dockerignore")).exists()).toBe( + isContainer, + ); }); test.each([ @@ -386,10 +398,6 @@ describe("project add runtime", () => { "strands-python only supports HTTP", ["--name", "my_agent", "--template", "strands-python", "--protocol", "MCP"], ], - [ - "strands-python only supports CodeZip builds", - ["--name", "my_agent", "--template", "strands-python", "--build", "Container"], - ], [ "invalid JSON in --network-config", ["--name", "my_agent", ...template, "--network-config", "{bad}"], diff --git a/src/handlers/project/project.test.ts b/src/handlers/project/project.test.ts index 742a4965c..686289229 100644 --- a/src/handlers/project/project.test.ts +++ b/src/handlers/project/project.test.ts @@ -163,6 +163,51 @@ describe("project create", () => { expect(await Bun.file(join(projectRoot, "app", "custom_agent", "main.py")).exists()).toBe(true); }); + test("scaffolds a Container agent from the strands template", async () => { + const directory = await inTempDirectory(); + await run([ + "create", + "--name", + "MyProject", + "--template", + "strands-python", + "--build", + "Container", + "--skip-install", + "--skip-git", + ]); + + const projectRoot = join(directory, "MyProject"); + const spec = await Bun.file(join(projectRoot, "agentcore", "agentcore.json")).json(); + expect(spec.runtimes[0]).toMatchObject({ + name: "strands_agent", + build: "Container", + codeLocation: "app/strands_agent", + dockerfile: "Dockerfile", + }); + expect(spec.runtimes[0].runtimeVersion).toBeUndefined(); + const runtimeRoot = join(projectRoot, "app", "strands_agent"); + expect(await Bun.file(join(runtimeRoot, "Dockerfile")).exists()).toBe(true); + expect(await Bun.file(join(runtimeRoot, ".dockerignore")).exists()).toBe(true); + }); + + test("omits the Dockerfile from a CodeZip strands template", async () => { + const directory = await inTempDirectory(); + await run([ + "create", + "--name", + "MyProject", + "--template", + "strands-python", + "--skip-install", + "--skip-git", + ]); + + const runtimeRoot = join(directory, "MyProject", "app", "strands_agent"); + expect(await Bun.file(join(runtimeRoot, "Dockerfile")).exists()).toBe(false); + expect(await Bun.file(join(runtimeRoot, ".dockerignore")).exists()).toBe(false); + }); + test.each([ ["default", [], ["SEMANTIC", "USER_PREFERENCE", "SUMMARIZATION", "EPISODIC"]], ["none", ["--memory", "none"], []], From d1c96c008e5dc5d24a7d36245e409983a5d2b7e3 Mon Sep 17 00:00:00 2001 From: Hweinstock Date: Fri, 28 Aug 2026 17:25:04 +0000 Subject: [PATCH 2/2] docs(template): add comment about why we strip the extension for containers --- src/core/project/templates/runtime.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/core/project/templates/runtime.ts b/src/core/project/templates/runtime.ts index a3e40fe4b..39e538393 100644 --- a/src/core/project/templates/runtime.ts +++ b/src/core/project/templates/runtime.ts @@ -113,6 +113,7 @@ const getTemplateResolvers = (assetSource: AssetSource, templateRenderer: Templa needsOs: filesystemConfigurations.length > 0, hasConfigBundle: false, enableOtel: true, + // entrypoint is only consumed on container path, where the docker file attempts to launch it as a module (without .py extension) entrypoint: input.scaffoldRuntimeInput.entrypoint.replace(/\.py$/, ""), }; const isContainer = input.scaffoldRuntimeInput.build === "Container";