From ce36381eab39af340a2eb0e71f5e5eb7e38f9ca5 Mon Sep 17 00:00:00 2001 From: Aidan Daly <99039782+aidandaly24@users.noreply.github.com> Date: Thu, 1 Oct 2026 14:39:43 -0400 Subject: [PATCH 1/5] fix(tui): omit redundant CLI-only menu dividers (#2493) * fix(tui): omit redundant CLI-only menu dividers * test(tui): cover redundant divider removal and row budgeting --- src/components/CliOnlyScreen.test.tsx | 19 ++++++----- src/components/RouterScreen.test.tsx | 49 +++++++++++++++++++++++++++ src/components/RouterScreen.tsx | 5 ++- 3 files changed, 64 insertions(+), 9 deletions(-) diff --git a/src/components/CliOnlyScreen.test.tsx b/src/components/CliOnlyScreen.test.tsx index 0812a4118..65cc00ea7 100644 --- a/src/components/CliOnlyScreen.test.tsx +++ b/src/components/CliOnlyScreen.test.tsx @@ -39,7 +39,7 @@ function cliOnlyCommands( const CLI_ONLY = cliOnlyCommands(); -describe("menus list command-line-only subcommands below a divider", () => { +describe("menus separate mixed command groups with a divider", () => { test("the root menu", async () => { const r = renderScreen("/agentcore", { withContext: inProjectContext }); @@ -77,13 +77,14 @@ describe("menus list command-line-only subcommands below a divider", () => { r.unmount(); }); - test("a menu whose every subcommand is command line only", async () => { + test("a menu whose every subcommand is command line only has no redundant divider", async () => { const r = renderScreen("/agentcore/eval/ondemand"); - await waitFor(() => hasCliDivider(r.lastFrame()!)); + await waitForText(r.lastFrame, "❯ evaluate"); + expect(hasCliDivider(r.lastFrame()!)).toBe(false); expect(menuEntries(r.lastFrame()!)).toEqual({ - screens: [], - cliOnly: ["evaluate", "simulate"], + screens: ["evaluate", "simulate"], + cliOnly: [], }); r.unmount(); }); @@ -133,10 +134,12 @@ describe("every command-line-only command opens on screen", () => { const parent = command.parent!; if (command.commands.length > 0) { - // A group opens its own menu, with every child under the divider. + // A CLI-only group opens its own menu without a redundant separator. await waitForText(r.lastFrame, path.join(" → ")); - await waitFor(() => hasCliDivider(r.lastFrame()!)); - expect(menuEntries(r.lastFrame()!).screens).toEqual([]); + expect(hasCliDivider(r.lastFrame()!)).toBe(false); + expect(menuEntries(r.lastFrame()!).screens).toEqual( + command.commands.map((child) => child.name()), + ); } else { await waitForText(r.lastFrame, "this command runs from the command line"); const help = command.createHelp(); diff --git a/src/components/RouterScreen.test.tsx b/src/components/RouterScreen.test.tsx index 7af18b3b4..f81a30231 100644 --- a/src/components/RouterScreen.test.tsx +++ b/src/components/RouterScreen.test.tsx @@ -64,6 +64,28 @@ function menuGroups(frame: string): { title: string | undefined; names: string[] // rendered frames — behavior a user would see, not internal state. describe("menu rendering", () => { + test.each([ + "/agentcore/log", + "/agentcore/traces", + "/agentcore/harness/traces", + "/agentcore/runtime/traces", + "/agentcore/payment", + "/agentcore/payment/connector", + "/agentcore/payment/manager", + "/agentcore/payment/session", + "/agentcore/payment/instrument", + ])("%s has no extra divider above a CLI-only menu", async (path) => { + const r = renderScreen(path, { withContext: inProjectContext }); + await waitForText(r.lastFrame, "type to choose a command"); + + const frame = r.lastFrame()!; + const lines = frame.split("\n"); + const filterRow = lines.findIndex((line) => /^\s*\/ /.test(line)); + expect(lines[filterRow + 2]).toMatch(/^\s*❯ /); + expect(hasCliDivider(frame)).toBe(false); + r.unmount(); + }); + test("CLI-only command names use the same white and focused colors as other commands", () => { // A separate process keeps ANSI enabled without changing the plain-text screen tests. const result = spawnSync( @@ -316,6 +338,21 @@ describe("narrow terminals", () => { }); describe("filtering", () => { + test("removes the CLI-only divider when filtering leaves only that group and restores it", async () => { + const r = renderScreen("/agentcore"); + await waitForText(r.lastFrame, "❯ create"); + expect(hasCliDivider(r.lastFrame()!)).toBe(true); + + await r.write("feedback"); + await waitForText(r.lastFrame, "❯ feedback"); + expect(hasCliDivider(r.lastFrame()!)).toBe(false); + + for (const _ of "feedback") await r.press("backspace"); + await waitForText(r.lastFrame, "❯ create"); + expect(hasCliDivider(r.lastFrame()!)).toBe(true); + r.unmount(); + }); + test("typing narrows the options to matches", async () => { const r = renderScreen("/agentcore/harness"); await waitForText(r.lastFrame, "list"); @@ -433,6 +470,18 @@ describe("navigation", () => { describe("short terminals", () => { const ROWS = 15; + test("does not reserve a row for a redundant CLI-only divider", async () => { + const r = renderScreen("/agentcore/log", { withContext: inProjectContext }); + await waitForText(r.lastFrame, "❯ runtime"); + await r.resize(100, 8); + + expect(r.lastFrame()).toContain("❯ runtime"); + expect(r.lastFrame()).toContain("harness"); + expect(r.lastFrame()).not.toContain("more"); + expect(hasCliDivider(r.lastFrame()!)).toBe(false); + r.unmount(); + }); + // fullMenu renders the root menu at the default height, where every option fits. async function fullMenu() { const r = renderScreen("/agentcore", { withContext: inProjectContext }); diff --git a/src/components/RouterScreen.tsx b/src/components/RouterScreen.tsx index 9ea9e974d..55da1cdff 100644 --- a/src/components/RouterScreen.tsx +++ b/src/components/RouterScreen.tsx @@ -289,7 +289,10 @@ function CommandMenuBody({ query, onQueryChange, }: CommandMenuBodyProps) { - const sections = useMemo(() => filtered.map((option) => option.section), [filtered]); + const sections = useMemo(() => { + const onlyCli = filtered.every((option) => option.section === CLI_ONLY_SECTION); + return filtered.map((option) => (onlyCli ? undefined : option.section)); + }, [filtered]); const menuHeight = Math.max(0, contentRows - FILTER_ROWS - alertRows(alert, columns)); const windowStart = Math.max(0, highlight - Math.floor(menuHeight / 2)); const view = scrollWindow({ From c1c88c8024770b6ac97626c5996c92f43e86a168 Mon Sep 17 00:00:00 2001 From: Aidan Daly <99039782+aidandaly24@users.noreply.github.com> Date: Thu, 1 Oct 2026 15:08:00 -0400 Subject: [PATCH 2/5] fix(templates): refresh Python dependencies and container hygiene (#2495) * fix(templates): allow patched MCP 1.x releases * fix(templates): require patched AgentCore Python SDK versions * fix(container): apply Debian package updates during builds * fix(container): omit uv cache from runtime images * fix(container): remove uv after dependency installation * fix(templates): honor SDK Strands integration dependencies * docs: remove unnecessary Python migration guide * test(project): remove redundant dependency assertion tests * test(templates): guard Python security ranges and container cleanup * fix(deps): patch Fastify audit findings in the lockfile --- bun.lock | 2 +- .../a2a-python-strands/pyproject.toml | 4 +- .../agent-python-minimal/pyproject.toml | 2 +- .../agent-python-strands/Dockerfile.template | 5 ++- .../agent-python-strands/pyproject.toml | 12 ++--- .../agui-python-strands/pyproject.toml | 4 +- .../export-harness-python/pyproject.toml | 6 +-- .../mcp-python-fastmcp/pyproject.toml | 2 +- src/core/project/manager.export.test.ts | 18 ++++++-- .../project/templates/pythonSecurity.test.ts | 44 +++++++++++++++++++ 10 files changed, 78 insertions(+), 21 deletions(-) create mode 100644 src/core/project/templates/pythonSecurity.test.ts diff --git a/bun.lock b/bun.lock index 8cc361b53..c0a6157ed 100644 --- a/bun.lock +++ b/bun.lock @@ -835,7 +835,7 @@ "fast-uri": ["fast-uri@4.1.5", "", {}, "sha512-vZeoMRB4epNr7QfdHxel7te/RcX16CxyXI07JCCTFWZA2s4v1azGNESRj+2EoaHSaWFL/Z3GmKT2jF6A202jLg=="], - "fastify": ["fastify@5.12.1", "", { "dependencies": { "@fastify/ajv-compiler": "^4.0.5", "@fastify/error": "^4.0.0", "@fastify/fast-json-stringify-compiler": "^5.0.0", "@fastify/proxy-addr": "^5.0.0", "abstract-logging": "^2.0.1", "avvio": "^9.0.0", "fast-json-stringify": "^7.0.0", "find-my-way": "^9.6.0", "light-my-request": "^6.0.0", "pino": "^9.14.0 || ^10.1.0", "process-warning": "^5.1.0", "rfdc": "^1.3.1", "secure-json-parse": "^4.0.0", "semver": "^7.6.0", "toad-cache": "^3.7.0" } }, "sha512-FWi+tQvwxR/PeRX7Z2mhfEF5ozJ3jn9asiiclzKXNSzJRHAYcU924aIOKAdHFJ+YIKieh3cqr1IwCOvTr41B3Q=="], + "fastify": ["fastify@5.12.5", "", { "dependencies": { "@fastify/ajv-compiler": "^4.0.5", "@fastify/error": "^4.0.0", "@fastify/fast-json-stringify-compiler": "^5.0.0", "@fastify/proxy-addr": "^5.0.0", "abstract-logging": "^2.0.1", "avvio": "^9.0.0", "fast-json-stringify": "^7.0.0", "find-my-way": "^9.6.0", "light-my-request": "^6.0.0", "pino": "^9.14.0 || ^10.1.0", "process-warning": "^5.1.0", "rfdc": "^1.3.1", "secure-json-parse": "^4.0.0", "semver": "^7.6.0", "toad-cache": "^3.7.0" } }, "sha512-OB2k1dlxs5/NAABqeKV2FUHkSD2BbENsCak8yULVcymn3fHIPDVa9TI3SDnJSWYSllZmSYuZXy2gTnsT+Sut1A=="], "fastify-plugin": ["fastify-plugin@5.1.0", "", {}, "sha512-FAIDA8eovSt5qcDgcBvDuX/v0Cjz0ohGhENZ/wpc3y+oZCY2afZ9Baqql3g/lC+OHRnciQol4ww7tuthOb9idw=="], diff --git a/src/assets/templates/a2a-python-strands/pyproject.toml b/src/assets/templates/a2a-python-strands/pyproject.toml index ede95b864..fe05495bd 100644 --- a/src/assets/templates/a2a-python-strands/pyproject.toml +++ b/src/assets/templates/a2a-python-strands/pyproject.toml @@ -11,9 +11,9 @@ requires-python = ">=3.10" dependencies = [ "a2a-sdk[all] ~= 0.3.0", "aws-opentelemetry-distro ~= 0.18.0", - "bedrock-agentcore[a2a] ~= 1.9.1", + "bedrock-agentcore[a2a,strands-agents] >= 1.18.1, < 2.0.0", "botocore[crt] ~= 1.43.0", - "strands-agents ~= 1.15.0", + "strands-agents >= 1.15.0, < 2.0.0", ] [tool.hatch.build.targets.wheel] diff --git a/src/assets/templates/agent-python-minimal/pyproject.toml b/src/assets/templates/agent-python-minimal/pyproject.toml index c7b4e3ca2..381256f35 100644 --- a/src/assets/templates/agent-python-minimal/pyproject.toml +++ b/src/assets/templates/agent-python-minimal/pyproject.toml @@ -10,7 +10,7 @@ readme = "README.md" requires-python = ">=3.10" dependencies = [ "aws-opentelemetry-distro ~= 0.18.0", - "bedrock-agentcore ~= 1.9.1", + "bedrock-agentcore >= 1.18.1, < 2.0.0", "botocore[crt] ~= 1.43.0", ] diff --git a/src/assets/templates/agent-python-strands/Dockerfile.template b/src/assets/templates/agent-python-strands/Dockerfile.template index cb3569eff..319ad9ba2 100644 --- a/src/assets/templates/agent-python-strands/Dockerfile.template +++ b/src/assets/templates/agent-python-strands/Dockerfile.template @@ -1,5 +1,7 @@ FROM public.ecr.aws/docker/library/python:3.12-slim-trixie +RUN apt-get update && apt-get upgrade -y && rm -rf /var/lib/apt/lists/* + RUN pip install --no-cache-dir uv ARG UV_DEFAULT_INDEX @@ -8,6 +10,7 @@ ARG UV_INDEX WORKDIR /app ENV UV_SYSTEM_PYTHON=1 \ + UV_NO_CACHE=1 \ UV_COMPILE_BYTECODE=1 \ UV_NO_PROGRESS=1 \ PYTHONUNBUFFERED=1 \ @@ -22,7 +25,7 @@ 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 +RUN uv sync --frozen --no-dev && /usr/local/bin/python -m pip uninstall -y uv USER bedrock_agentcore diff --git a/src/assets/templates/agent-python-strands/pyproject.toml b/src/assets/templates/agent-python-strands/pyproject.toml index e21492965..61ad4cf53 100644 --- a/src/assets/templates/agent-python-strands/pyproject.toml +++ b/src/assets/templates/agent-python-strands/pyproject.toml @@ -10,13 +10,13 @@ readme = "README.md" requires-python = ">=3.10" dependencies = [ "aws-opentelemetry-distro ~= 0.18.0", - "bedrock-agentcore ~= 1.9.1", + "bedrock-agentcore[strands-agents] >= 1.18.1, < 2.0.0", "botocore[crt] ~= 1.43.0", - {{#if (eq modelProvider "Anthropic")}}"strands-agents[anthropic] ~= 1.15.0", - {{else}}{{#if (eq modelProvider "OpenAI")}}"strands-agents[openai] ~= 1.15.0", - {{else}}{{#if (eq modelProvider "Gemini")}}"strands-agents[gemini] ~= 1.15.0", - {{else}}{{#if (eq modelProvider "LiteLLM")}}"strands-agents[litellm] ~= 1.15.0", - {{else}}"strands-agents ~= 1.15.0", + {{#if (eq modelProvider "Anthropic")}}"strands-agents[anthropic] >= 1.15.0, < 2.0.0", + {{else}}{{#if (eq modelProvider "OpenAI")}}"strands-agents[openai] >= 1.15.0, < 2.0.0", + {{else}}{{#if (eq modelProvider "Gemini")}}"strands-agents[gemini] >= 1.15.0, < 2.0.0", + {{else}}{{#if (eq modelProvider "LiteLLM")}}"strands-agents[litellm] >= 1.15.0, < 2.0.0", + {{else}}"strands-agents >= 1.15.0, < 2.0.0", {{/if}}{{/if}}{{/if}}{{/if}} ] diff --git a/src/assets/templates/agui-python-strands/pyproject.toml b/src/assets/templates/agui-python-strands/pyproject.toml index ab778db75..96eaa3e65 100644 --- a/src/assets/templates/agui-python-strands/pyproject.toml +++ b/src/assets/templates/agui-python-strands/pyproject.toml @@ -12,9 +12,9 @@ dependencies = [ "ag-ui-strands >= 0.3.0, < 0.4.0", "ag-ui-protocol >= 0.1.19, < 0.2.0", "aws-opentelemetry-distro ~= 0.18.0", - "bedrock-agentcore ~= 1.9.1", + "bedrock-agentcore[strands-agents] >= 1.18.1, < 2.0.0", "botocore[crt] ~= 1.43.0", - "strands-agents ~= 1.15.0", + "strands-agents >= 1.15.0, < 2.0.0", "uvicorn >= 0.34.3, < 1.0.0", ] diff --git a/src/assets/templates/export-harness-python/pyproject.toml b/src/assets/templates/export-harness-python/pyproject.toml index 29262d715..3649109d2 100644 --- a/src/assets/templates/export-harness-python/pyproject.toml +++ b/src/assets/templates/export-harness-python/pyproject.toml @@ -10,11 +10,11 @@ readme = "README.md" requires-python = ">=3.10" dependencies = [ "aws-opentelemetry-distro ~= 0.18.0", - "bedrock-agentcore ~= 1.9.1", + "bedrock-agentcore[strands-agents] >= 1.18.1, < 2.0.0", "botocore[crt] ~= 1.43.0", - "mcp >= 1.23.0, < 2.0.0", + "mcp >= 1.28.1, < 2.0.0", {{#if bedrockMantle}}"aws-bedrock-token-generator >= 1.1.0, < 2.0.0", - {{/if}}"strands-agents{{#if strandsExtras}}[{{strandsExtras}}]{{/if}} ~= 1.54.0", + {{/if}}"strands-agents{{#if strandsExtras}}[{{strandsExtras}}]{{/if}} >= 1.54.0, < 2.0.0", ] diff --git a/src/assets/templates/mcp-python-fastmcp/pyproject.toml b/src/assets/templates/mcp-python-fastmcp/pyproject.toml index 960168bc5..f9febaf96 100644 --- a/src/assets/templates/mcp-python-fastmcp/pyproject.toml +++ b/src/assets/templates/mcp-python-fastmcp/pyproject.toml @@ -10,7 +10,7 @@ readme = "README.md" requires-python = ">=3.10" dependencies = [ "aws-opentelemetry-distro ~= 0.18.0", - "mcp ~= 1.24.0", + "mcp >= 1.28.1, < 2.0.0", ] [tool.hatch.build.targets.wheel] diff --git a/src/core/project/manager.export.test.ts b/src/core/project/manager.export.test.ts index de3f458ec..8d70ddfef 100644 --- a/src/core/project/manager.export.test.ts +++ b/src/core/project/manager.export.test.ts @@ -86,6 +86,16 @@ function exportInput(overrides: Partial = {}): ExportHarness } describe("FsProjectManager.exportHarness rendered tree", () => { + test("an exported harness permits the framework version selected by the SDK integration", async () => { + const { manager: subject } = manager(); + const project = await projectWithHarness(subject); + + const result = await drain(subject.exportHarness(project, exportInput())); + + const pyproject = await Bun.file(join(result.agentPath, "pyproject.toml")).text(); + expect(pyproject).toContain('"strands-agents >= 1.54.0, < 2.0.0"'); + }); + test("loads only the MCP tools allowedTools selects", async () => { const { manager: subject } = manager(); const project = await projectWithHarness(subject, { @@ -238,7 +248,7 @@ describe("FsProjectManager.exportHarness rendered tree", () => { expect(loadModel).toContain('params["temperature"] = 0.2'); expect(loadModel).toContain('params["top_p"] = 0.8'); const pyproject = await Bun.file(join(result.agentPath, "pyproject.toml")).text(); - expect(pyproject).toContain('"strands-agents[openai] ~= 1.54.0"'); + expect(pyproject).toContain('"strands-agents[openai] >= 1.54.0, < 2.0.0"'); expect(pyproject).not.toContain('"openai ~= 1.0.0"'); }); @@ -265,7 +275,7 @@ describe("FsProjectManager.exportHarness rendered tree", () => { expect(loadModel).toContain('params["top_p"] = 0.9'); expect(loadModel).toContain('params["top_k"] = 20'); expect(await Bun.file(join(result.agentPath, "pyproject.toml")).text()).toContain( - '"strands-agents[gemini] ~= 1.54.0"', + '"strands-agents[gemini] >= 1.54.0, < 2.0.0"', ); }); @@ -290,7 +300,7 @@ describe("FsProjectManager.exportHarness rendered tree", () => { expect(loadModel).toContain('params["top_p"] = 0.7'); expect(loadModel).toContain('json.loads("{\\"max_retries\\":2}")'); expect(await Bun.file(join(result.agentPath, "pyproject.toml")).text()).toContain( - '"strands-agents[litellm] ~= 1.54.0"', + '"strands-agents[litellm] >= 1.54.0, < 2.0.0"', ); }); @@ -310,7 +320,7 @@ describe("FsProjectManager.exportHarness rendered tree", () => { expect(main).toContain("from strands import AgentSkills"); expect(main).toContain('SlidingWindowConversationManager(**{"window_size":12}, per_turn=True)'); expect(await Bun.file(join(result.agentPath, "pyproject.toml")).text()).toContain( - '"strands-agents ~= 1.54.0"', + '"strands-agents >= 1.54.0, < 2.0.0"', ); }); diff --git a/src/core/project/templates/pythonSecurity.test.ts b/src/core/project/templates/pythonSecurity.test.ts new file mode 100644 index 000000000..c5ff66225 --- /dev/null +++ b/src/core/project/templates/pythonSecurity.test.ts @@ -0,0 +1,44 @@ +import { expect, test } from "bun:test"; +import { FsAssetSource } from "../source"; +import { HandlebarsTemplateRenderer } from "./renderer"; + +const source = new FsAssetSource(); +const renderer = new HandlebarsTemplateRenderer(); + +test.each([ + ["agent-python-minimal", ["bedrock-agentcore >= 1.18.1, < 2.0.0"]], + ["agent-python-strands", ["bedrock-agentcore[strands-agents] >= 1.18.1, < 2.0.0"]], + ["a2a-python-strands", ["bedrock-agentcore[a2a,strands-agents] >= 1.18.1, < 2.0.0"]], + ["agui-python-strands", ["bedrock-agentcore[strands-agents] >= 1.18.1, < 2.0.0"]], + [ + "export-harness-python", + ["bedrock-agentcore[strands-agents] >= 1.18.1, < 2.0.0", "mcp >= 1.28.1, < 2.0.0"], + ], + ["mcp-python-fastmcp", ["mcp >= 1.28.1, < 2.0.0"]], +] as const)("%s retains patched Python dependency ranges", async (template, dependencies) => { + const pyproject = renderer.render(await source.read(`templates/${template}/pyproject.toml`), { + name: "test_agent", + modelProvider: "Bedrock", + }); + + expect(Bun.TOML.parse(pyproject)).toMatchObject({ + project: { dependencies: expect.arrayContaining([...dependencies]) }, + }); +}); + +test("the Strands container updates OS packages and removes uv after installing dependencies", async () => { + const dockerfile = renderer.render( + await source.read("templates/agent-python-strands/Dockerfile.template"), + { entrypoint: "main" }, + ); + + expect(dockerfile).toMatch( + /RUN apt-get update && apt-get upgrade -y[\s\S]*USER bedrock_agentcore/, + ); + expect(dockerfile).toMatch( + /UV_NO_CACHE=1[\s\S]*RUN uv sync --frozen --no-dev --no-install-project/, + ); + expect(dockerfile).toMatch( + /RUN uv sync --frozen --no-dev && \/usr\/local\/bin\/python -m pip uninstall -y uv[\s\S]*USER bedrock_agentcore/, + ); +}); From 4939c1b7f190a82c4664a42219630266110d2497 Mon Sep 17 00:00:00 2001 From: "agentcore-devx-automation[bot]" <282717993+agentcore-devx-automation[bot]@users.noreply.github.com> Date: Thu, 1 Oct 2026 15:32:28 -0400 Subject: [PATCH 3/5] chore(release): v1.0.0-rc.5 (#2502) Co-authored-by: github-actions[bot] --- command.md | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/command.md b/command.md index 106553f70..c0ee06392 100644 --- a/command.md +++ b/command.md @@ -2,7 +2,7 @@ # AgentCore CLI command reference -This reference was generated from `agentcore --help` for version `1.0.0-rc.4`. +This reference was generated from `agentcore --help` for version `1.0.0-rc.5`. ## Table of contents diff --git a/package.json b/package.json index 78e7a17d6..3ab4e1e9e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@aws/agentcore", - "version": "1.0.0-rc.4", + "version": "1.0.0-rc.5", "repository": { "type": "git", "url": "https://github.com/aws/agentcore-cli.git" From 70a932ca4ba380643552b3e7c0d68878a84e96da Mon Sep 17 00:00:00 2001 From: Hweinstock <42325418+Hweinstock@users.noreply.github.com> Date: Thu, 1 Oct 2026 16:02:58 -0400 Subject: [PATCH 4/5] fix(bma): rename environment template and skip unsupported dev (#2501) * fix(templates): name BMA environment-python-bma * fix(dev): skip unsupported BMA environments * fix(deps): patch Fastify audit findings --- src/core/project/manager.test.ts | 12 +-- src/core/project/manager.tsx | 15 +--- src/core/project/templates/runtime.ts | 4 +- src/errors/errors.tsx | 4 +- .../project/add/runtime/index.test.ts | 45 ++++++----- src/handlers/project/add/runtime/index.ts | 4 +- .../add/runtime/runtime.screen.test.tsx | 4 +- src/handlers/project/add/runtime/screen.tsx | 2 +- .../project/{bmaProfile.ts => bma.ts} | 12 ++- .../project/create/create.screen.test.tsx | 8 +- src/handlers/project/create/region.ts | 3 +- src/handlers/project/create/screen.tsx | 2 +- src/handlers/project/dev/index.test.ts | 75 +++++++++++++++++-- src/handlers/project/dev/index.ts | 46 +++++++++--- src/handlers/project/project.test.ts | 4 +- .../project/runtimeTemplateProfile.test.ts | 6 +- .../project/runtimeTemplateProfile.ts | 4 +- src/handlers/project/shortcuts.test.ts | 6 +- src/handlers/project/shortcuts.ts | 10 +-- src/handlers/project/types.ts | 2 +- 20 files changed, 182 insertions(+), 86 deletions(-) rename src/handlers/project/{bmaProfile.ts => bma.ts} (70%) diff --git a/src/core/project/manager.test.ts b/src/core/project/manager.test.ts index fd7f49011..abda94a7d 100644 --- a/src/core/project/manager.test.ts +++ b/src/core/project/manager.test.ts @@ -47,7 +47,7 @@ const AGENT_PYTHON_STRANDS_CONTAINER = resolveRuntimeTemplateShortcut( const AGENT_TYPESCRIPT_STRANDS = resolveRuntimeTemplateShortcut("agent-typescript-strands"); const A2A_PYTHON_STRANDS = resolveRuntimeTemplateShortcut("a2a-python-strands"); const AGENT_PYTHON_LANGCHAIN = resolveRuntimeTemplateShortcut("agent-python-langchain"); -const BEDROCK_MANAGED_AGENTS = resolveRuntimeTemplateShortcut("bedrock-managed-agents"); +const BEDROCK_MANAGED_AGENTS = resolveRuntimeTemplateShortcut("environment-python-bma"); function withTemplateProfile( input: ScaffoldRuntimeInput, @@ -301,14 +301,14 @@ describe("FsProjectManager.create", () => { }); const projectRoot = join(directory, "example"); - const appDir = join(projectRoot, "app", "bedrock_managed_agents"); + const appDir = join(projectRoot, "app", "environment_python_bma"); const spec = await Bun.file(join(projectRoot, "agentcore", "agentcore.json")).json(); expect(spec.runtimes).toEqual([ { - name: "bedrock_managed_agents", + name: "environment_python_bma", build: "Container", entrypoint: "lifecycle/server.py", - codeLocation: "app/bedrock_managed_agents", + codeLocation: "app/environment_python_bma", dockerfile: "Dockerfile", additionalPolicies: ["bma-acr-policy.json"], protocol: "HTTP", @@ -328,7 +328,7 @@ describe("FsProjectManager.create", () => { ).exists(), ).toBe(true); expect(await Bun.file(join(appDir, "pyproject.toml")).text()).toContain( - 'name = "bedrock_managed_agents"', + 'name = "environment_python_bma"', ); expect(setup.commands).toEqual([ { @@ -950,7 +950,7 @@ describe("FsProjectManager.addResource", () => { const { error } = await deployOutcome(subject, project); expect(error).toBeInstanceOf(RegionUnsupportedFeatureError); - expect(String(error)).toContain("runtime 'bedrock_managed_agents'"); + expect(String(error)).toContain("runtime 'environment_python_bma'"); expect(String(error)).toContain(BMA_CN_MESSAGE); expect(deployCalls).toEqual([]); }); diff --git a/src/core/project/manager.tsx b/src/core/project/manager.tsx index 7543f6e9e..371217896 100644 --- a/src/core/project/manager.tsx +++ b/src/core/project/manager.tsx @@ -94,12 +94,7 @@ import type { CreateCloudFormationClient } from "../types"; import type { CoreIdentityClient } from "../../handlers/identity/types"; import { templateManagesDependencies } from "../../handlers/project/templateProfile"; import { resolveRuntimeTemplateProfile } from "../../handlers/project/runtimeTemplateProfile"; -import { - BMA_POLICY_FILE, - BMA_TEMPLATE_NAME, - BMA_TEMPLATE_TAG_KEY, - BMA_TEMPLATE_TAG_VALUE, -} from "../../handlers/project/bmaProfile"; +import { isBmaRuntime } from "../../handlers/project/bma"; const TARGETS_EXAMPLE = '[{ "name": "default", "account": "111122223333", "region": "us-east-1" }]'; @@ -469,7 +464,7 @@ export class FsProjectManager implements ProjectManager { if (input.resourceType === "runtime") { const { framework, modelProvider, modelId, memory } = input.resourceConfig.scaffoldRuntimeInput; - if (framework === BMA_TEMPLATE_NAME) { + if (framework === "bma") { throw new RegionUnsupportedFeatureError(BMA_CN_MESSAGE); } if (framework !== "none") { @@ -1167,11 +1162,7 @@ export class FsProjectManager implements ProjectManager { // modelProvider (BYO, provider-free, hand-edited, or scaffolded by an // older CLI) cannot be classified and only get an informational note. if (isChinaRegion(target.region)) { - const bmaRuntimes = project.spec.runtimes.filter( - (runtime) => - runtime.tags?.[BMA_TEMPLATE_TAG_KEY] === BMA_TEMPLATE_TAG_VALUE || - runtime.additionalPolicies?.includes(BMA_POLICY_FILE), - ); + const bmaRuntimes = project.spec.runtimes.filter(isBmaRuntime); if (bmaRuntimes.length > 0) { throw new RegionUnsupportedFeatureError( `Cannot deploy to China region ${target.region}: ` + diff --git a/src/core/project/templates/runtime.ts b/src/core/project/templates/runtime.ts index ea2004fce..a7b33f117 100644 --- a/src/core/project/templates/runtime.ts +++ b/src/core/project/templates/runtime.ts @@ -331,9 +331,7 @@ const getTemplateResolvers = (assetSource: AssetSource, templateRenderer: Templa spec: { runtimes: [{ ...buildRuntimeSpec(input), protocol: "HTTP" as const }] }, }; }, - [buildResolverKey("bedrock-managed-agents", "Python", "HTTP")]: async ( - input: RuntimeResourceConfig, - ) => { + [buildResolverKey("bma", "Python", "HTTP")]: async (input: RuntimeResourceConfig) => { const tree = await FsTreeNode.fromAssetSource( { assetSource }, { assetDir: "templates/bedrock-managed-agents" }, diff --git a/src/errors/errors.tsx b/src/errors/errors.tsx index 06f1c12cc..25552c3a0 100644 --- a/src/errors/errors.tsx +++ b/src/errors/errors.tsx @@ -108,8 +108,8 @@ export class ResourceNotFoundError extends AgentCoreCLIError { /** Error raised when a command or operation has not been implemented yet. */ export class NotImplementedError extends AgentCoreCLIError { - constructor(message?: string, options?: Omit) { - super(message ?? "not implemented yet", { ...options, source: ERROR_SOURCE.INTERNAL }); + constructor(message?: string, options?: AgentCoreCLIErrorOptions) { + super(message ?? "not implemented yet", { source: ERROR_SOURCE.INTERNAL, ...options }); } } diff --git a/src/handlers/project/add/runtime/index.test.ts b/src/handlers/project/add/runtime/index.test.ts index fdfce409a..892d38806 100644 --- a/src/handlers/project/add/runtime/index.test.ts +++ b/src/handlers/project/add/runtime/index.test.ts @@ -104,7 +104,7 @@ describe("project add runtime", () => { build: "CodeZip", protocol: "AGUI", }, - "bedrock-managed-agents template preset": { + "environment-python-bma template preset": { build: "Container", entrypoint: "lifecycle/server.py", dockerfile: "Dockerfile", @@ -176,8 +176,8 @@ describe("project add runtime", () => { ["--name", "my_agui", "--template", "agui-python-strands"], ], [ - "bedrock-managed-agents template preset", - ["--name", "my_bma", "--template", "bedrock-managed-agents"], + "environment-python-bma template preset", + ["--name", "my_bma", "--template", "environment-python-bma"], ], [ "agent-python-strands with session, EFS, and S3 mounts", @@ -286,14 +286,14 @@ describe("project add runtime", () => { const spec = await Bun.file(join(projectRoot, "agentcore", "agentcore.json")).json(); const runtime = spec.runtimes.find((candidate: { name: string }) => candidate.name === name); expect(runtime).toMatchObject({ entrypoint: "main.py", ...expectedSpecByLabel[label] }); - const isBma = flags.includes("bedrock-managed-agents"); + const isBma = flags.includes("environment-python-bma"); expect( await Bun.file( join(projectRoot, "app", name, isBma ? "lifecycle/server.py" : "main.py"), ).exists(), ).toBe(true); const isContainer = flags.some( - (flag) => flag.endsWith("-container") || flag === "bedrock-managed-agents", + (flag) => flag.endsWith("-container") || flag === "environment-python-bma", ); expect(runtime.runtimeVersion).toBe(isContainer ? undefined : "PYTHON_3_14"); expect(await Bun.file(join(projectRoot, "app", name, "Dockerfile")).exists()).toBe(isContainer); @@ -311,7 +311,7 @@ describe("project add runtime", () => { "--name", "my_bma", "--template", - "bedrock-managed-agents", + "environment-python-bma", ]); expect(core.projectCommands).toEqual([]); @@ -329,7 +329,7 @@ describe("project add runtime", () => { "--name", "my_bma", "--template", - "bedrock-managed-agents", + "environment-python-bma", "--lifecycle-configuration", '{"idleRuntimeSessionTimeout":300,"maxLifetime":3600}', "--additional-policies", @@ -363,7 +363,7 @@ describe("project add runtime", () => { "--name", "my_bma", "--template", - "bedrock-managed-agents", + "environment-python-bma", "--role-arn", roleArn, "--json", @@ -391,7 +391,7 @@ describe("project add runtime", () => { ["agent-typescript-vercel", []], ["mcp-python-fastmcp", []], ["agui-python-strands", ["SEMANTIC", "USER_PREFERENCE", "SUMMARIZATION", "EPISODIC"]], - ["bedrock-managed-agents", []], + ["environment-python-bma", []], ])("%s ships with its pre-configured memory", async (templateName, expectedStrategies) => { const { projectRoot, cleanup } = await initProject(); cleanups.push(cleanup); @@ -526,8 +526,9 @@ describe("project add runtime", () => { ], ], [ - "--model-provider is not valid with the bedrock-managed-agents template", - ["--name", "my_bma", "--template", "bedrock-managed-agents", "--model-provider", "Anthropic"], + "--model-provider is not valid with the environment-python-bma template", + ["--name", "my_bma", "--template", "environment-python-bma", "--model-provider", "Anthropic"], + "--model-provider, --model-id, and --api-key are not valid with the environment-python-bma template", ], [ "--model-provider without a template requires agent-python-strands", @@ -545,13 +546,21 @@ describe("project add runtime", () => { await expectError(promise, requiredMessage ?? /./, InputValidationError); }); - test("rejects an unknown --template value", async () => { - const { cleanup } = await initProject(); - cleanups.push(cleanup); - await expect( - run(["add", "runtime", "--name", "my_agent", "--template", "nonsense"]), - ).rejects.toThrow(); - }); + test.each(["nonsense", "bedrock-managed-agents"])( + "rejects the unknown --template value %s", + async (template) => { + const { projectRoot, cleanup } = await initProject(); + cleanups.push(cleanup); + await expect( + run(["add", "runtime", "--name", "my_agent", "--template", template]), + ).rejects.toThrow(); + const spec = await Bun.file(join(projectRoot, "agentcore", "agentcore.json")).json(); + expect(spec.runtimes).toEqual([]); + expect( + await Bun.file(join(projectRoot, "app", "my_agent", "lifecycle", "server.py")).exists(), + ).toBe(false); + }, + ); }); describe("project add runtime --type import", () => { diff --git a/src/handlers/project/add/runtime/index.ts b/src/handlers/project/add/runtime/index.ts index a7114677b..efc4e5fd1 100644 --- a/src/handlers/project/add/runtime/index.ts +++ b/src/handlers/project/add/runtime/index.ts @@ -23,7 +23,7 @@ import { } from "../../importBedrockAgent"; import { RegionKey } from "../../../keys"; import { addProjectResource, requireDeployedNameFits } from "../shared"; -import { BMA_CUSTOM_EXECUTION_ROLE_WARNING, BMA_TEMPLATE_NAME } from "../../bmaProfile"; +import { BMA_CUSTOM_EXECUTION_ROLE_WARNING } from "../../bma"; const CONFIGURATION = "Configuration:"; const ENVIRONMENT = "Environment:"; @@ -275,7 +275,7 @@ export const createAddRuntimeHandler = (config: AddProjectResourceConfig) => }) : resolveRuntimeTemplateShortcut("agent-python-minimal", { runtimeName: flags.name }); - if (scaffoldRuntimeInput.framework === BMA_TEMPLATE_NAME && flags["role-arn"] !== undefined) { + if (scaffoldRuntimeInput.framework === "bma" && flags["role-arn"] !== undefined) { notes.push(BMA_CUSTOM_EXECUTION_ROLE_WARNING); } diff --git a/src/handlers/project/add/runtime/runtime.screen.test.tsx b/src/handlers/project/add/runtime/runtime.screen.test.tsx index 023c67191..09937ba97 100644 --- a/src/handlers/project/add/runtime/runtime.screen.test.tsx +++ b/src/handlers/project/add/runtime/runtime.screen.test.tsx @@ -142,10 +142,11 @@ describe("project add runtime wizard", () => { await r.press("return"); await waitForText(r.lastFrame, "choose a template"); - await selectTemplate(r, "bedrock-managed-agents"); + await selectTemplate(r, "environment-python-bma"); await r.press("return"); await waitForFlatText(r.lastFrame, "build Container"); + expect(flatFrame(r.lastFrame)).toContain("template environment-python-bma"); await r.press("return"); await waitForText(r.lastFrame, "added runtime 'bma_environment' to 'TestProject'"); @@ -153,6 +154,7 @@ describe("project add runtime wizard", () => { build: "Container", entrypoint: "lifecycle/server.py", additionalPolicies: ["bma-acr-policy.json"], + tags: { "agentcore:template": "BedrockManagedAgents" }, }); expect( await Bun.file( diff --git a/src/handlers/project/add/runtime/screen.tsx b/src/handlers/project/add/runtime/screen.tsx index 62bfeca0c..dafb22a77 100644 --- a/src/handlers/project/add/runtime/screen.tsx +++ b/src/handlers/project/add/runtime/screen.tsx @@ -119,7 +119,7 @@ function AddRuntimeWizard({ project, core }: { project: Project; core: ScreenPro - help="the agent code scaffolded into app/" choices={TEMPLATE_CHOICES} value={values.template} diff --git a/src/handlers/project/bmaProfile.ts b/src/handlers/project/bma.ts similarity index 70% rename from src/handlers/project/bmaProfile.ts rename to src/handlers/project/bma.ts index 5a7881d28..c229b4701 100644 --- a/src/handlers/project/bmaProfile.ts +++ b/src/handlers/project/bma.ts @@ -1,10 +1,20 @@ import type { RuntimeTemplateProfile } from "./templateProfile"; +import type { ProjectRuntime } from "../../projectSchemas/runtime"; -export const BMA_TEMPLATE_NAME = "bedrock-managed-agents"; +export const BMA_TEMPLATE_NAME = "environment-python-bma"; export const BMA_POLICY_FILE = "bma-acr-policy.json"; export const BMA_TEMPLATE_TAG_KEY = "agentcore:template"; export const BMA_TEMPLATE_TAG_VALUE = "BedrockManagedAgents"; +export function isBmaRuntime( + runtime: Pick, +): boolean { + return ( + runtime.tags?.[BMA_TEMPLATE_TAG_KEY] === BMA_TEMPLATE_TAG_VALUE || + runtime.additionalPolicies?.includes(BMA_POLICY_FILE) === true + ); +} + export const BMA_CUSTOM_EXECUTION_ROLE_WARNING = `Warning: --role-arn uses an existing execution role, so AgentCore CDK cannot attach ` + `${BMA_POLICY_FILE}. Ensure the role grants bedrock-mantle:RegisterEnvironment and ` + diff --git a/src/handlers/project/create/create.screen.test.tsx b/src/handlers/project/create/create.screen.test.tsx index afdb5ce6d..94f267b1f 100644 --- a/src/handlers/project/create/create.screen.test.tsx +++ b/src/handlers/project/create/create.screen.test.tsx @@ -389,7 +389,7 @@ describe("project create wizard", () => { expect(r.lastFrame()).toContain("● agent-python-strands"); expect(r.lastFrame()).not.toContain("agent-python-strands (recommended)"); expect(r.lastFrame()).toContain("agent-python-strands-container"); - expect(r.lastFrame()).toContain("bedrock-managed-agents"); + expect(r.lastFrame()).toContain("environment-python-bma"); await r.press("return"); // No memory step: memory is no longer a choice, so review follows directly. @@ -438,8 +438,8 @@ describe("project create wizard", () => { await waitForText(r.lastFrame, "choose a template"); await r.press("down"); // agent-python-strands-container await r.press("down"); // agent-python-langchain - await r.press("down"); // bedrock-managed-agents - await waitForText(r.lastFrame, "● bedrock-managed-agents"); + await r.press("down"); // environment-python-bma + await waitForText(r.lastFrame, "● environment-python-bma"); await r.press("return"); await waitForText(r.lastFrame, "this project will be created"); await r.press("return"); @@ -465,7 +465,7 @@ describe("project create wizard", () => { await waitForText(r.lastFrame, "choose a template"); await r.press("down"); // agent-python-strands-container await r.press("down"); // agent-python-langchain - await r.press("down"); // bedrock-managed-agents + await r.press("down"); // environment-python-bma await r.press("down"); // agent-python-minimal await waitForText(r.lastFrame, "● agent-python-minimal "); await r.press("return"); diff --git a/src/handlers/project/create/region.ts b/src/handlers/project/create/region.ts index ef695af01..266e64da5 100644 --- a/src/handlers/project/create/region.ts +++ b/src/handlers/project/create/region.ts @@ -7,7 +7,6 @@ import { MODEL_PROVIDER_RUNTIMES_CN_MESSAGE, } from "../../../core/project/manager"; import { RegionUnsupportedFeatureError } from "../../../errors"; -import { BMA_TEMPLATE_NAME } from "../bmaProfile"; import type { CreateProjectInput } from "../types"; /** Applies the hard create-time restrictions shared by the CLI and interactive wizard. */ @@ -20,7 +19,7 @@ export function validateCreateRegionSupport(input: CreateProjectInput, region: s const runtime = input.scaffoldRuntimeInput; if (runtime === undefined || runtime.framework === "none") return; - if (runtime.framework === BMA_TEMPLATE_NAME) { + if (runtime.framework === "bma") { throw new RegionUnsupportedFeatureError(BMA_CN_MESSAGE); } if ((runtime.modelProvider ?? "Bedrock") !== "LiteLLM") { diff --git a/src/handlers/project/create/screen.tsx b/src/handlers/project/create/screen.tsx index 8330cfd24..25e502fb6 100644 --- a/src/handlers/project/create/screen.tsx +++ b/src/handlers/project/create/screen.tsx @@ -202,7 +202,7 @@ export function ProjectCreateScreen({ ctx, core }: ScreenProps) { {values.kind === "agent" && ( - choices={TEMPLATE_CHOICES} value={values.template} onChange={(template) => patch({ template })} diff --git a/src/handlers/project/dev/index.test.ts b/src/handlers/project/dev/index.test.ts index f21759010..8af08b8c6 100644 --- a/src/handlers/project/dev/index.test.ts +++ b/src/handlers/project/dev/index.test.ts @@ -3,6 +3,7 @@ import { join } from "node:path"; import type { ProjectRuntime } from "../../../projectSchemas/runtime"; import { InputValidationError, + NotImplementedError, ResourceNotFoundError, SilentCLIError, UserCancellationError, @@ -13,6 +14,12 @@ import { testIO } from "../../../testing"; import { JsonRendererKey } from "../../../tui"; import { JsonKey, RegionKey } from "../../keys"; import type { Project } from "../types"; +import { + BMA_POLICY_FILE, + BMA_TEMPLATE_NAME, + BMA_TEMPLATE_TAG_KEY, + BMA_TEMPLATE_TAG_VALUE, +} from "../bma"; import { createDevProjectHandler, type DevProjectHandlerConfig } from "."; import type { DevEnvironmentInput } from "./environment"; import type { DevEvent, DevRunner, DevServerInput, DevTraceCollector } from "./types"; @@ -35,6 +42,15 @@ function project(...runtimes: ProjectRuntime[]): Project { }; } +function bmaRuntime(overrides: Partial = {}): ProjectRuntime { + return { + ...runtime("environment", "Container"), + tags: { [BMA_TEMPLATE_TAG_KEY]: BMA_TEMPLATE_TAG_VALUE }, + additionalPolicies: [BMA_POLICY_FILE], + ...overrides, + }; +} + function captureRunner(events: DevEvent[] = []) { const inputs: DevServerInput[] = []; const runner: DevRunner = { @@ -187,6 +203,32 @@ async function inspectorStatus(subject: ReturnType): Promise<{ n } describe("project dev selection and dispatch", () => { + test.each([ + ["headless", undefined, project(bmaRuntime({ additionalPolicies: undefined }))], + ["browser", undefined, project(bmaRuntime({ tags: undefined }))], + [ + "headless", + "environment", + project(bmaRuntime({ tags: { [BMA_TEMPLATE_TAG_KEY]: "Custom" } }), runtime()), + ], + ["browser", "environment", project(bmaRuntime(), runtime())], + ] as const)( + "rejects unsupported BMA selection (%s, %s)", + async (mode, agent, configuredProject) => { + const subject = harness({ project: configuredProject }); + const pending = subject.run({ mode, agent }); + await expect(pending).rejects.toBeInstanceOf(NotImplementedError); + await expect(pending).rejects.toMatchObject({ source: "user", exitCode: 1 }); + await expect(pending).rejects.toThrow( + `Local dev is not supported for runtime 'environment' (${BMA_TEMPLATE_NAME})`, + ); + expect(subject.codeZip.inputs).toHaveLength(0); + expect(subject.container.inputs).toHaveLength(0); + expect(subject.collector.starts).toHaveLength(0); + expect(subject.ui.starts).toHaveLength(0); + }, + ); + test.each([ [project(), {}, "This project has no runtimes", InputValidationError], [ @@ -212,7 +254,7 @@ describe("project dev selection and dispatch", () => { test("loads the environment and dispatches the selected runtime", async () => { const subject = harness({ - project: project(runtime("orders"), runtime("support", "Container")), + project: project(bmaRuntime(), runtime("orders"), runtime("support", "Container")), }); await subject.run({ agent: "support", port: 4567 }); @@ -235,6 +277,7 @@ describe("project dev selection and dispatch", () => { }, runtime: { name: "support", build: "Container" }, }); + expect(subject.io.stderr()).not.toContain("Skipping runtime"); }); test("announces an automatically selected port", async () => { @@ -264,12 +307,21 @@ describe("project dev headless multi-agent", () => { return { pending }; } - test("supervises every runtime with attributed output and per-runtime env", async () => { + test("supervises supported runtimes with attributed output and per-runtime env", async () => { const codeZip = stayingRunner([{ type: "stdout", line: "orders says hi" }]); const container = stayingRunner(); - const subject = harness({ project: twoRuntimes(), codeZip, container }); + const subject = harness({ + project: project( + bmaRuntime({ tags: { [BMA_TEMPLATE_TAG_KEY]: "Custom" } }), + runtime("orders"), + runtime("support", "Container"), + ), + codeZip, + container, + }); const { pending } = await supervised(subject); + expect(subject.io.stderr()).toContain("Skipping runtime 'environment'"); expect(codeZip.inputs).toHaveLength(1); expect(container.inputs).toHaveLength(1); expect(codeZip.inputs[0]!.env).toMatchObject({ @@ -429,9 +481,9 @@ describe("project dev Inspector UI mode", () => { }, ); - test("serves the Inspector API: status lists every runtime, none started", async () => { + test("serves the Inspector API: status lists supported runtimes, none started", async () => { const subject = harness({ - project: project(runtime("orders"), runtime("support", "Container")), + project: project(bmaRuntime(), runtime("orders"), runtime("support", "Container")), }); const { pending } = await runUi(subject); @@ -446,7 +498,9 @@ describe("project dev Inspector UI mode", () => { }); test("agentcore.json edits reload the supervised agents", async () => { - const subject = harness({ reloadedRuntimes: [runtime("orders"), runtime("payments")] }); + const subject = harness({ + reloadedRuntimes: [bmaRuntime({ tags: undefined }), runtime("orders"), runtime("payments")], + }); const { pending } = await runUi(subject); expect(subject.watchers[0]?.path).toBe( @@ -467,11 +521,12 @@ describe("project dev Inspector UI mode", () => { test("--agent narrows the supervised set", async () => { const subject = harness({ - project: project(runtime("orders"), runtime("support", "Container")), + project: project(bmaRuntime(), runtime("orders"), runtime("support", "Container")), }); const { pending } = await runUi(subject, { agent: "support" }); expect((await inspectorStatus(subject)).map((agent) => agent.name)).toEqual(["support"]); + expect(subject.io.stderr()).not.toContain("Skipping runtime"); process.emit("SIGINT", "SIGINT"); await pending.catch(() => undefined); @@ -493,7 +548,11 @@ test("project dev renders attributed human and NDJSON output", async () => { ]; for (const json of [false, true]) { - const subject = harness({ codeZip: captureRunner(events), json }); + const subject = harness({ + project: project(bmaRuntime(), runtime()), + codeZip: captureRunner(events), + json, + }); await subject.run({ agent: "orders", traces: false }); expect(subject.io.stdout()).toBe( json diff --git a/src/handlers/project/dev/index.ts b/src/handlers/project/dev/index.ts index 7a389e34e..de3603cdd 100644 --- a/src/handlers/project/dev/index.ts +++ b/src/handlers/project/dev/index.ts @@ -9,7 +9,9 @@ import { DevSupervisor, type SupervisorConfig } from "../../../core/dev/supervis import type { ProjectRuntime } from "../../../projectSchemas/runtime"; import { AgentCoreCLIError, + ERROR_SOURCE, InputValidationError, + NotImplementedError, ResourceNotFoundError, SilentCLIError, UserCancellationError, @@ -19,6 +21,7 @@ import { createHandler, flag, ProjectKey, type Middleware } from "../../../route import { JsonRendererKey, type JsonRenderer } from "../../../tui"; import { JsonKey, RegionKey } from "../../keys"; import type { Project, ProjectManager } from "../types"; +import { BMA_TEMPLATE_NAME, isBmaRuntime } from "../bma"; import type { DevEnvironmentLoader } from "./environment"; import type { DevEvent, DevRunner, DevTraceCollector, DevTraceCollectorStarter } from "./types"; @@ -54,20 +57,34 @@ function otelEnvForRuntime( return runtime.build === "Container" ? rewriteOtelEndpointForContainer(env) : env; } +function supportsLocalDev(runtime: ProjectRuntime): boolean { + return !isBmaRuntime(runtime); +} + function selectRuntimes(project: Project, name?: string): ProjectRuntime[] { if (project.spec.runtimes.length === 0) { throw new InputValidationError( "This project has no runtimes. Add a runtime to agentcore/agentcore.json and retry.", ); } - if (!name) return project.spec.runtimes; - - const runtime = project.spec.runtimes.find((candidate) => candidate.name === name); - if (runtime) return [runtime]; - const available = project.spec.runtimes.map((candidate) => candidate.name).join(", "); - throw new ResourceNotFoundError( - `Runtime '${name}' was not found. Available runtimes: ${available}.`, - ); + const selectedRuntimes = name + ? project.spec.runtimes.filter((runtime) => runtime.name === name) + : project.spec.runtimes; + if (selectedRuntimes.length === 0) { + const available = project.spec.runtimes.map((candidate) => candidate.name).join(", "); + throw new ResourceNotFoundError( + `Runtime '${name}' was not found. Available runtimes: ${available}.`, + ); + } + const supportedRuntimes = selectedRuntimes.filter(supportsLocalDev); + if (supportedRuntimes.length === 0) { + throw new NotImplementedError( + `Local dev is not supported for runtime '${selectedRuntimes[0]!.name}' (${BMA_TEMPLATE_NAME}). ` + + "Run agentcore deploy, then use client.py to connect through Bedrock Managed Agents.", + { source: ERROR_SOURCE.USER }, + ); + } + return supportedRuntimes; } /** An agent's own output, always tagged with the agent that produced it. */ @@ -136,6 +153,17 @@ export const createDevProjectHandler = (config: DevProjectHandlerConfig) => "--port applies to a single runtime. Use --agent to select one.", ); } + if (!flags.agent) { + for (const runtime of project.spec.runtimes.filter( + (runtime) => !supportsLocalDev(runtime), + )) { + renderStatus( + config.io, + `Skipping runtime '${runtime.name}': local dev is not supported for ${BMA_TEMPLATE_NAME}.`, + json, + ); + } + } if ( flags.traces && @@ -264,7 +292,7 @@ export const createDevProjectHandler = (config: DevProjectHandlerConfig) => try { const reloaded = await config.projectManager.resolve({ filePath: project.rootPath }); if (!reloaded) return; - const runtimes = reloaded.spec.runtimes; + const runtimes = reloaded.spec.runtimes.filter(supportsLocalDev); supervisor.setRuntimes( flags.agent ? runtimes.filter((runtime) => runtime.name === flags.agent) : runtimes, ); diff --git a/src/handlers/project/project.test.ts b/src/handlers/project/project.test.ts index 99ea93165..226f03281 100644 --- a/src/handlers/project/project.test.ts +++ b/src/handlers/project/project.test.ts @@ -359,7 +359,7 @@ describe("project create", () => { "--name", "BmaProject", "--template", - "bedrock-managed-agents", + "environment-python-bma", ]); const projectRoot = join(directory, "BmaProject"); @@ -1206,7 +1206,7 @@ describe("create in China regions", () => { "--name", "CnBma", "--template", - "bedrock-managed-agents", + "environment-python-bma", ...skips, "--region", "cn-north-1", diff --git a/src/handlers/project/runtimeTemplateProfile.test.ts b/src/handlers/project/runtimeTemplateProfile.test.ts index 579423382..853603b26 100644 --- a/src/handlers/project/runtimeTemplateProfile.test.ts +++ b/src/handlers/project/runtimeTemplateProfile.test.ts @@ -1,18 +1,18 @@ import { describe, expect, test } from "bun:test"; -import { BMA_TEMPLATE_PROFILE } from "./bmaProfile"; +import { BMA_TEMPLATE_PROFILE } from "./bma"; import { resolveRuntimeTemplateProfile } from "./runtimeTemplateProfile"; describe("resolveRuntimeTemplateProfile", () => { test("derives the canonical BMA profile from the framework", () => { expect( resolveRuntimeTemplateProfile({ - framework: "bedrock-managed-agents", + framework: "bma", }), ).toBe(BMA_TEMPLATE_PROFILE); expect( resolveRuntimeTemplateProfile({ - framework: "bedrock-managed-agents", + framework: "bma", templateProfile: { usesModel: true, dependencySetup: "managed", diff --git a/src/handlers/project/runtimeTemplateProfile.ts b/src/handlers/project/runtimeTemplateProfile.ts index de23fe400..d6b5ca017 100644 --- a/src/handlers/project/runtimeTemplateProfile.ts +++ b/src/handlers/project/runtimeTemplateProfile.ts @@ -1,4 +1,4 @@ -import { BMA_TEMPLATE_NAME, BMA_TEMPLATE_PROFILE } from "./bmaProfile"; +import { BMA_TEMPLATE_PROFILE } from "./bma"; import type { RuntimeTemplateProfile } from "./templateProfile"; import type { ScaffoldRuntimeInput } from "./types"; @@ -11,7 +11,7 @@ import type { ScaffoldRuntimeInput } from "./types"; export function resolveRuntimeTemplateProfile( input: Pick, ): RuntimeTemplateProfile | undefined { - if (input.framework === BMA_TEMPLATE_NAME) { + if (input.framework === "bma") { return BMA_TEMPLATE_PROFILE; } return input.templateProfile; diff --git a/src/handlers/project/shortcuts.test.ts b/src/handlers/project/shortcuts.test.ts index 299f4cc8b..28b7ce7d8 100644 --- a/src/handlers/project/shortcuts.test.ts +++ b/src/handlers/project/shortcuts.test.ts @@ -14,7 +14,7 @@ describe("template order", () => { "agent-python-strands", "agent-python-strands-container", "agent-python-langchain", - "bedrock-managed-agents", + "environment-python-bma", "agent-python-minimal", "agent-typescript-strands", "agent-typescript-vercel", @@ -27,10 +27,10 @@ describe("template order", () => { }); test("the Bedrock Managed Agents shortcut selects its environment profile", () => { - expect(RUNTIME_TEMPLATE_SHORTCUTS["bedrock-managed-agents"]).toMatchObject({ + expect(RUNTIME_TEMPLATE_SHORTCUTS["environment-python-bma"]).toMatchObject({ build: "Container", language: "Python", - framework: "bedrock-managed-agents", + framework: "bma", protocol: "HTTP", includesMemory: false, supportsModelProviderOverride: false, diff --git a/src/handlers/project/shortcuts.ts b/src/handlers/project/shortcuts.ts index 06e942011..06dad9166 100644 --- a/src/handlers/project/shortcuts.ts +++ b/src/handlers/project/shortcuts.ts @@ -8,7 +8,7 @@ import { import { InputValidationError } from "../../errors"; import { ScaffoldRuntimeInputSchema, type ModelProvider, type ScaffoldRuntimeInput } from "./types"; import type { RuntimeTemplateProfile } from "./templateProfile"; -import { BMA_TEMPLATE_PROFILE } from "./bmaProfile"; +import { BMA_TEMPLATE_NAME, BMA_TEMPLATE_PROFILE } from "./bma"; /** The default memory that templates ship with. */ export function getDefaultMemorySpec(runtimeName: string): Memory { @@ -117,12 +117,12 @@ export const RUNTIME_TEMPLATE_SHORTCUTS = { supportsModelProviderOverride: false, runtimeVersion: "NODE_22", }, - "bedrock-managed-agents": { - runtimeName: "bedrock_managed_agents", + [BMA_TEMPLATE_NAME]: { + runtimeName: "environment_python_bma", description: "Execution environment for Bedrock Managed Agents", build: "Container", language: "Python", - framework: "bedrock-managed-agents", + framework: "bma", protocol: "HTTP", includesMemory: false, supportsModelProviderOverride: false, @@ -181,7 +181,7 @@ const FRAMEWORK_ORDER: Record = { strands: 0, langchain: 1, vercelai: 2, - "bedrock-managed-agents": 3, + bma: 3, none: 4, }; const BUILD_ORDER: Record = { CodeZip: 0, Container: 1 }; diff --git a/src/handlers/project/types.ts b/src/handlers/project/types.ts index 40322a7fb..26ad94657 100644 --- a/src/handlers/project/types.ts +++ b/src/handlers/project/types.ts @@ -48,7 +48,7 @@ export const ScaffoldRuntimeInputSchema = z runtimeName: AgentNameSchema, build: BuildTypeSchema, language: z.enum(["Python", "TypeScript"]), - framework: z.enum(["strands", "langchain", "vercelai", "bedrock-managed-agents", "none"]), + framework: z.enum(["strands", "langchain", "vercelai", "bma", "none"]), protocol: ProtocolModeSchema.optional(), modelProvider: ModelProviderSchema.optional(), modelId: z.string().min(1).optional(), From 733c43741846b56202452f6ea0734f88a2b6a1e2 Mon Sep 17 00:00:00 2001 From: Hweinstock Date: Thu, 1 Oct 2026 20:26:01 +0000 Subject: [PATCH 5/5] feat(ci): add hourly tagged E2E canary --- .github/workflows/canary.yml | 50 +++++++++++++++++++++++++++++++ .github/workflows/e2e-test.yml | 35 ++++++++++++---------- e2eTest/constants.ts | 1 + e2eTest/project/harness.test.ts | 3 +- e2eTest/project/templates.test.ts | 2 +- 5 files changed, 74 insertions(+), 17 deletions(-) create mode 100644 .github/workflows/canary.yml diff --git a/.github/workflows/canary.yml b/.github/workflows/canary.yml new file mode 100644 index 000000000..5d6fe8a10 --- /dev/null +++ b/.github/workflows/canary.yml @@ -0,0 +1,50 @@ +name: canary +on: + schedule: + - cron: "0 * * * *" + workflow_dispatch: + +concurrency: + group: canary + cancel-in-progress: false + +permissions: + id-token: write + contents: read + +jobs: + # Resolve the commit hash for the latest published @rc version. + resolve_release: + runs-on: ubuntu-latest + outputs: + ref: ${{ steps.resolve.outputs.ref }} + steps: + - uses: actions/setup-node@v4 + with: + node-version: "24" + - name: Resolve latest RC release commit + id: resolve + run: | + version="$(npm view @aws/agentcore@rc version)" + tag_ref="refs/tags/v${version}" + # Resolve annotated tags to their commit hash when needed. + ref="$(git ls-remote https://github.com/aws/agentcore-cli.git "${tag_ref}" "${tag_ref}^{}" | awk -v tag_ref="${tag_ref}" ' + $2 == tag_ref "^{}" { print $1; found = 1; exit } + $2 == tag_ref { tag_sha = $1 } + END { if (!found && tag_sha != "") print tag_sha } + ')" + if [[ -z "${ref}" ]]; then + echo "::error::No upstream tag found for @aws/agentcore@rc version ${version}" + exit 1 + fi + echo "ref=${ref}" >> "$GITHUB_OUTPUT" + + e2e: + needs: resolve_release + uses: ./.github/workflows/e2e-test.yml + with: + ref: ${{ needs.resolve_release.outputs.ref }} + tags: canary + linuxOnly: true + secrets: + WORKFLOW_SECRETS_READER_ROLE_ARN: ${{ secrets.WORKFLOW_SECRETS_READER_ROLE_ARN }} diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml index 5bb32afcd..11bd502c3 100644 --- a/.github/workflows/e2e-test.yml +++ b/.github/workflows/e2e-test.yml @@ -12,6 +12,11 @@ on: required: false type: string default: "" + linuxOnly: + description: Run E2E tests only on Linux + required: false + type: boolean + default: false secrets: WORKFLOW_SECRETS_READER_ROLE_ARN: required: true @@ -25,9 +30,13 @@ on: description: Vitest tag expression, for example runtime || canary type: string default: "" + linuxOnly: + description: Run E2E tests only on Linux + type: boolean + default: false concurrency: - group: e2e-test-${{ inputs.ref || github.ref }}-${{ inputs.tags || 'all' }} + group: e2e-test-${{ inputs.ref || github.ref }}-${{ inputs.tags || 'all' }}-${{ inputs.linuxOnly && 'linux-only' || 'all-os' }} cancel-in-progress: false env: @@ -40,7 +49,7 @@ jobs: id-token: write contents: read outputs: - is-authorized: ${{ steps.check.outputs.is_authorized }} + is-authorized: ${{ github.workflow == 'canary' || steps.check.outputs.is_authorized == 'true' }} steps: - name: Fetch secrets from Secrets Manager uses: aws/agentcore-devx-devtools/.github/actions/fetch-secrets@75989f65f7f193deaf83c237c36572d1a8f800b2 @@ -71,25 +80,21 @@ jobs: name: test (${{ matrix.name }}) needs: authorize if: needs.authorize.outputs.is-authorized == 'true' - runs-on: ${{ fromJSON(matrix.runner) }} + runs-on: >- + ${{ fromJSON( + matrix.name == 'Linux' && + format('["codebuild-agentcore-e2e-{0}-{1}","e2e-linux"]', github.run_id, github.run_attempt) + || matrix.name == 'Windows' && + format('["codebuild-agentcore-e2e-{0}-{1}","image:windows-1.0","e2e-windows"]', github.run_id, github.run_attempt) + || matrix.name == 'macOS' && '["macos-latest"]' + ) }} permissions: id-token: write contents: read strategy: fail-fast: false matrix: - include: - # Per-job labels stop GitHub from routing a job to the runner CodeBuild created for another job. - # https://docs.aws.amazon.com/codebuild/latest/userguide/sample-github-action-runners-update-labels.html - - name: Linux - runner: '["codebuild-agentcore-e2e-${{ github.run_id }}-${{ github.run_attempt }}", "e2e-linux"]' - - name: Windows - runner: '["codebuild-agentcore-e2e-${{ github.run_id }}-${{ github.run_attempt }}", "image:windows-1.0", "e2e-windows"]' - - # CodeBuild does not support macOS. - # https://docs.aws.amazon.com/codebuild/latest/userguide/action-runner-questions.html#action-runner-platform - - name: macOS - runner: '["macos-latest"]' + name: ${{ fromJSON(inputs.linuxOnly && '["Linux"]' || '["Linux","Windows","macOS"]') }} steps: - uses: actions/checkout@v7 with: diff --git a/e2eTest/constants.ts b/e2eTest/constants.ts index 9eb966a72..c10c7dd98 100644 --- a/e2eTest/constants.ts +++ b/e2eTest/constants.ts @@ -1,6 +1,7 @@ export const E2E_PREFIX = "e2e"; export const TAGS = { + CANARY: "canary", HARNESS: "harness", RUNTIME: "runtime", } as const; diff --git a/e2eTest/project/harness.test.ts b/e2eTest/project/harness.test.ts index 39c567a84..71edc840a 100644 --- a/e2eTest/project/harness.test.ts +++ b/e2eTest/project/harness.test.ts @@ -58,7 +58,8 @@ const HarnessInvokeResponseSchema = z.object({ transcript: z.array(TranscriptItemSchema).min(2), }); -describe("add, deploy, and invoke harnesses", { sequential: true, tags: [TAGS.HARNESS] }, () => { +const harnessTags = [TAGS.HARNESS, TAGS.CANARY]; +describe("add, deploy, and invoke harnesses", { sequential: true, tags: harnessTags }, () => { const cli = new CliRunner(); const projectName = `${E2E_PREFIX}${Date.now().toString(36)}`; let projectDir: string; diff --git a/e2eTest/project/templates.test.ts b/e2eTest/project/templates.test.ts index 7e1f28064..80262b716 100644 --- a/e2eTest/project/templates.test.ts +++ b/e2eTest/project/templates.test.ts @@ -146,7 +146,7 @@ const A2aResponseSchema = z.object({ describe( "add, dev, deploy, invoke for runtime templates", - { sequential: true, tags: [TAGS.RUNTIME] }, + { sequential: true, tags: [TAGS.RUNTIME, TAGS.CANARY] }, () => { const cli = new CliRunner(); const projectName = `${E2E_PREFIX}${Date.now().toString(36)}`;