From cb839bf3e62e85b2a6b04763924aa3351f59a179 Mon Sep 17 00:00:00 2001 From: "T.J Ariyawansa" Date: Fri, 24 Apr 2026 22:35:02 -0400 Subject: [PATCH 01/18] feat: add runtime endpoint support to AgentCore CLI - Schema: endpoints field on AgentEnvSpec, runtimeVersion in deployed state - Primitive: RuntimeEndpointPrimitive with add/remove/preview - TUI: Add and Remove flows with multi-field form - Status: endpoints nested under agents with deployment badges - Deploy: parseRuntimeEndpointOutputs + buildDeployedState pipeline --- ...f1f00de3-3b7d-4c4e-ab58-812e212cf9a6.jsonl | 18 + .omc/state/hud-stdin-cache.json | 1 + .omc/state/idle-notif-cooldown.json | 5 + .omc/state/last-tool-error.json | 7 + .omc/state/mission-state.json | 157 ++ .../hud-state.json | 6 + .omc/state/subagent-tracking.json | 89 + docs/bug-bash-runtime-endpoints.md | 138 ++ .../2026-04-24-runtime-endpoint-support.md | 1593 +++++++++++++++++ src/cli/cloudformation/outputs.ts | 40 + src/cli/commands/deploy/actions.ts | 13 + src/cli/commands/remove/types.ts | 1 + src/cli/commands/status/action.ts | 28 +- src/cli/commands/status/command.tsx | 34 +- src/cli/logging/remove-logger.ts | 1 + .../primitives/RuntimeEndpointPrimitive.ts | 314 ++++ .../RuntimeEndpointPrimitive.test.ts | 14 + src/cli/primitives/index.ts | 3 + src/cli/primitives/registry.ts | 3 + src/cli/tui/components/ResourceGraph.tsx | 40 +- src/cli/tui/hooks/useRemove.ts | 23 + src/cli/tui/screens/add/AddFlow.tsx | 19 + src/cli/tui/screens/add/AddScreen.tsx | 1 + src/cli/tui/screens/remove/RemoveFlow.tsx | 110 +- .../remove/RemoveRuntimeEndpointScreen.tsx | 29 + src/cli/tui/screens/remove/RemoveScreen.tsx | 11 + .../remove/__tests__/RemoveScreen.test.tsx | 2 + src/cli/tui/screens/remove/index.ts | 1 + .../AddRuntimeEndpointFlow.tsx | 104 ++ .../AddRuntimeEndpointScreen.tsx | 246 +++ src/cli/tui/screens/runtime-endpoint/index.ts | 2 + src/cli/tui/screens/runtime-endpoint/types.ts | 8 + .../useAddRuntimeEndpointWizard.ts | 61 + .../schemas/__tests__/agent-env.test.ts | 128 ++ .../schemas/__tests__/deployed-state.test.ts | 32 + src/schema/schemas/agent-env.ts | 27 + src/schema/schemas/deployed-state.ts | 14 + 37 files changed, 3300 insertions(+), 23 deletions(-) create mode 100644 .omc/state/agent-replay-f1f00de3-3b7d-4c4e-ab58-812e212cf9a6.jsonl create mode 100644 .omc/state/hud-stdin-cache.json create mode 100644 .omc/state/idle-notif-cooldown.json create mode 100644 .omc/state/last-tool-error.json create mode 100644 .omc/state/mission-state.json create mode 100644 .omc/state/sessions/f1f00de3-3b7d-4c4e-ab58-812e212cf9a6/hud-state.json create mode 100644 .omc/state/subagent-tracking.json create mode 100644 docs/bug-bash-runtime-endpoints.md create mode 100644 docs/superpowers/plans/2026-04-24-runtime-endpoint-support.md create mode 100644 src/cli/primitives/RuntimeEndpointPrimitive.ts create mode 100644 src/cli/primitives/__tests__/RuntimeEndpointPrimitive.test.ts create mode 100644 src/cli/tui/screens/remove/RemoveRuntimeEndpointScreen.tsx create mode 100644 src/cli/tui/screens/runtime-endpoint/AddRuntimeEndpointFlow.tsx create mode 100644 src/cli/tui/screens/runtime-endpoint/AddRuntimeEndpointScreen.tsx create mode 100644 src/cli/tui/screens/runtime-endpoint/index.ts create mode 100644 src/cli/tui/screens/runtime-endpoint/types.ts create mode 100644 src/cli/tui/screens/runtime-endpoint/useAddRuntimeEndpointWizard.ts diff --git a/.omc/state/agent-replay-f1f00de3-3b7d-4c4e-ab58-812e212cf9a6.jsonl b/.omc/state/agent-replay-f1f00de3-3b7d-4c4e-ab58-812e212cf9a6.jsonl new file mode 100644 index 000000000..224d6e448 --- /dev/null +++ b/.omc/state/agent-replay-f1f00de3-3b7d-4c4e-ab58-812e212cf9a6.jsonl @@ -0,0 +1,18 @@ +{"t":0,"agent":"a85961b","agent_type":"general-purpose","event":"agent_start","parent_mode":"none"} +{"t":0,"agent":"ae4a7c0","agent_type":"general-purpose","event":"agent_start","parent_mode":"none"} +{"t":0,"agent":"ae4a7c0","agent_type":"general-purpose","event":"agent_stop","success":true,"duration_ms":93620} +{"t":0,"agent":"a85961b","agent_type":"general-purpose","event":"agent_stop","success":true,"duration_ms":136363} +{"t":0,"agent":"aa40468","agent_type":"general-purpose","event":"agent_start","parent_mode":"none"} +{"t":0,"agent":"a75b2ce","agent_type":"general-purpose","event":"agent_start","parent_mode":"none"} +{"t":0,"agent":"a75b2ce","agent_type":"general-purpose","event":"agent_stop","success":true,"duration_ms":43886} +{"t":0,"agent":"aa40468","agent_type":"general-purpose","event":"agent_stop","success":true,"duration_ms":85714} +{"t":0,"agent":"a91e852","agent_type":"general-purpose","event":"agent_start","parent_mode":"none"} +{"t":0,"agent":"af08908","agent_type":"general-purpose","event":"agent_start","parent_mode":"none"} +{"t":0,"agent":"a91e852","agent_type":"general-purpose","event":"agent_stop","success":true,"duration_ms":48913} +{"t":0,"agent":"af08908","agent_type":"general-purpose","event":"agent_stop","success":true,"duration_ms":168965} +{"t":0,"agent":"a7a88af","agent_type":"general-purpose","event":"agent_start","parent_mode":"none"} +{"t":0,"agent":"ac6e0fe","agent_type":"general-purpose","event":"agent_start","parent_mode":"none"} +{"t":0,"agent":"ac6e0fe","agent_type":"general-purpose","event":"agent_stop","success":true,"duration_ms":136371} +{"t":0,"agent":"aab94b9","agent_type":"general-purpose","event":"agent_start","parent_mode":"none"} +{"t":0,"agent":"aab94b9","agent_type":"general-purpose","event":"agent_stop","success":true,"duration_ms":420237} +{"t":0,"agent":"a7a88af","agent_type":"general-purpose","event":"agent_stop","success":true,"duration_ms":1365705} diff --git a/.omc/state/hud-stdin-cache.json b/.omc/state/hud-stdin-cache.json new file mode 100644 index 000000000..02c439e7a --- /dev/null +++ b/.omc/state/hud-stdin-cache.json @@ -0,0 +1 @@ +{"session_id":"f1f00de3-3b7d-4c4e-ab58-812e212cf9a6","transcript_path":"/Users/tjariy/.claude/projects/-Users-tjariy-github-workspace-runtime-endpoint-project/f1f00de3-3b7d-4c4e-ab58-812e212cf9a6.jsonl","cwd":"/Users/tjariy/github_workspace/runtime_endpoint_project/agentcore-cli","model":{"id":"us.anthropic.claude-opus-4-6-v1[1m]","display_name":"Opus 4.6 (1M context)"},"workspace":{"current_dir":"/Users/tjariy/github_workspace/runtime_endpoint_project/agentcore-cli","project_dir":"/Users/tjariy/github_workspace/runtime_endpoint_project","added_dirs":[]},"version":"2.1.92","output_style":{"name":"default"},"cost":{"total_cost_usd":91.16863265000003,"total_duration_ms":27828835,"total_api_duration_ms":4169427,"total_lines_added":3308,"total_lines_removed":156},"context_window":{"total_input_tokens":9962,"total_output_tokens":202304,"context_window_size":1000000,"current_usage":{"input_tokens":3,"output_tokens":150,"cache_creation_input_tokens":3313,"cache_read_input_tokens":377703},"used_percentage":38,"remaining_percentage":62},"exceeds_200k_tokens":true} \ No newline at end of file diff --git a/.omc/state/idle-notif-cooldown.json b/.omc/state/idle-notif-cooldown.json new file mode 100644 index 000000000..db471b019 --- /dev/null +++ b/.omc/state/idle-notif-cooldown.json @@ -0,0 +1,5 @@ +{ + "lastSentAt": "2026-04-24T22:12:31.660Z", + "repoSignature": "{\"repo\":\"aws/agentcore-cli\",\"headSha\":\"3d2d671b9513df7e3e31857bc246dbc11b02bcd9\",\"dirty\":true,\"openPrNumbers\":[580,630,638,650,660,693,718,726,727,740,752,776,794,802,807,855,862,868,876,912,913,914,915,916,917,926,950,952,953,954,955,959,960,961,962,963,964,965,966],\"openIssueNumbers\":[29,38,85,140,155,174,204,205,207,234,243,245,258,274,503,542,565,570,602,603,604,607,624,629,637,659,665,666,667,668,669,670,671,672,673,674,676,679,681,682,683,684,686,699,719,737,741,746,747,761,781,800,808,809,810,815,821,838,839,840,870,889,895,900,902,906,907,927,931,932,948],\"failingRunIds\":[24910302672,24910302910,24910304889,24910354876,24910355028,24910355044,24911643922,24911643929,24911914713,24912214011,24913252566]}", + "backlogZero": false +} \ No newline at end of file diff --git a/.omc/state/last-tool-error.json b/.omc/state/last-tool-error.json new file mode 100644 index 000000000..112e57c44 --- /dev/null +++ b/.omc/state/last-tool-error.json @@ -0,0 +1,7 @@ +{ + "tool_name": "Read", + "tool_input_preview": "{\"file_path\":\"/Users/tjariy/github_workspace/runtime_endpoint_project/agentcore-cli/src/schema/agent-env.ts\"}", + "error": "File does not exist. Note: your current working directory is /Users/tjariy/github_workspace/runtime_endpoint_project/agentcore-cli.", + "timestamp": "2026-04-24T19:40:50.234Z", + "retry_count": 1 +} \ No newline at end of file diff --git a/.omc/state/mission-state.json b/.omc/state/mission-state.json new file mode 100644 index 000000000..a616d5c1e --- /dev/null +++ b/.omc/state/mission-state.json @@ -0,0 +1,157 @@ +{ + "updatedAt": "2026-04-24T20:03:31.160Z", + "missions": [ + { + "id": "session:f1f00de3-3b7d-4c4e-ab58-812e212cf9a6:none", + "source": "session", + "name": "none", + "objective": "Session mission", + "createdAt": "2026-04-24T19:32:44.270Z", + "updatedAt": "2026-04-24T20:03:31.160Z", + "status": "done", + "workerCount": 9, + "taskCounts": { + "total": 9, + "pending": 0, + "blocked": 0, + "inProgress": 0, + "completed": 9, + "failed": 0 + }, + "agents": [ + { + "name": "general-purpose:a85961b", + "role": "general-purpose", + "ownership": "a85961b5e884a9c3f", + "status": "done", + "currentStep": null, + "latestUpdate": "completed", + "completedSummary": null, + "updatedAt": "2026-04-24T19:35:00.633Z" + }, + { + "name": "general-purpose:ae4a7c0", + "role": "general-purpose", + "ownership": "ae4a7c0082c191d0c", + "status": "done", + "currentStep": null, + "latestUpdate": "completed", + "completedSummary": null, + "updatedAt": "2026-04-24T19:34:17.959Z" + }, + { + "name": "general-purpose:aa40468", + "role": "general-purpose", + "ownership": "aa404682d61fd1e64", + "status": "done", + "currentStep": null, + "latestUpdate": "completed", + "completedSummary": null, + "updatedAt": "2026-04-24T19:37:02.609Z" + }, + { + "name": "general-purpose:a75b2ce", + "role": "general-purpose", + "ownership": "a75b2cea40e5cf3b8", + "status": "done", + "currentStep": null, + "latestUpdate": "completed", + "completedSummary": null, + "updatedAt": "2026-04-24T19:36:20.844Z" + }, + { + "name": "general-purpose:a91e852", + "role": "general-purpose", + "ownership": "a91e852ab81865a94", + "status": "done", + "currentStep": null, + "latestUpdate": "completed", + "completedSummary": null, + "updatedAt": "2026-04-24T19:37:58.704Z" + }, + { + "name": "general-purpose:af08908", + "role": "general-purpose", + "ownership": "af0890850bb863cd5", + "status": "done", + "currentStep": null, + "latestUpdate": "completed", + "completedSummary": null, + "updatedAt": "2026-04-24T19:39:58.838Z" + }, + { + "name": "general-purpose:a7a88af", + "role": "general-purpose", + "ownership": "a7a88afbb7c155ef8", + "status": "done", + "currentStep": null, + "latestUpdate": "completed", + "completedSummary": null, + "updatedAt": "2026-04-24T20:03:31.160Z" + }, + { + "name": "general-purpose:ac6e0fe", + "role": "general-purpose", + "ownership": "ac6e0fe11e842c942", + "status": "done", + "currentStep": null, + "latestUpdate": "completed", + "completedSummary": null, + "updatedAt": "2026-04-24T19:43:01.896Z" + }, + { + "name": "general-purpose:aab94b9", + "role": "general-purpose", + "ownership": "aab94b981437723ec", + "status": "done", + "currentStep": null, + "latestUpdate": "completed", + "completedSummary": null, + "updatedAt": "2026-04-24T19:52:37.964Z" + } + ], + "timeline": [ + { + "id": "session-start:ac6e0fe11e842c942:2026-04-24T19:40:45.525Z", + "at": "2026-04-24T19:40:45.525Z", + "kind": "update", + "agent": "general-purpose:ac6e0fe", + "detail": "started general-purpose:ac6e0fe", + "sourceKey": "session-start:ac6e0fe11e842c942" + }, + { + "id": "session-stop:ac6e0fe11e842c942:2026-04-24T19:43:01.896Z", + "at": "2026-04-24T19:43:01.896Z", + "kind": "completion", + "agent": "general-purpose:ac6e0fe", + "detail": "completed", + "sourceKey": "session-stop:ac6e0fe11e842c942" + }, + { + "id": "session-start:aab94b981437723ec:2026-04-24T19:45:37.727Z", + "at": "2026-04-24T19:45:37.727Z", + "kind": "update", + "agent": "general-purpose:aab94b9", + "detail": "started general-purpose:aab94b9", + "sourceKey": "session-start:aab94b981437723ec" + }, + { + "id": "session-stop:aab94b981437723ec:2026-04-24T19:52:37.964Z", + "at": "2026-04-24T19:52:37.964Z", + "kind": "completion", + "agent": "general-purpose:aab94b9", + "detail": "completed", + "sourceKey": "session-stop:aab94b981437723ec" + }, + { + "id": "session-stop:a7a88afbb7c155ef8:2026-04-24T20:03:31.160Z", + "at": "2026-04-24T20:03:31.160Z", + "kind": "completion", + "agent": "general-purpose:a7a88af", + "detail": "completed", + "sourceKey": "session-stop:a7a88afbb7c155ef8" + } + ] + } + ] +} \ No newline at end of file diff --git a/.omc/state/sessions/f1f00de3-3b7d-4c4e-ab58-812e212cf9a6/hud-state.json b/.omc/state/sessions/f1f00de3-3b7d-4c4e-ab58-812e212cf9a6/hud-state.json new file mode 100644 index 000000000..85fe1d642 --- /dev/null +++ b/.omc/state/sessions/f1f00de3-3b7d-4c4e-ab58-812e212cf9a6/hud-state.json @@ -0,0 +1,6 @@ +{ + "timestamp": "2026-04-24T19:31:15.877Z", + "backgroundTasks": [], + "sessionStartTimestamp": "2026-04-24T18:50:17.974Z", + "sessionId": "f1f00de3-3b7d-4c4e-ab58-812e212cf9a6" +} \ No newline at end of file diff --git a/.omc/state/subagent-tracking.json b/.omc/state/subagent-tracking.json new file mode 100644 index 000000000..e0e377e35 --- /dev/null +++ b/.omc/state/subagent-tracking.json @@ -0,0 +1,89 @@ +{ + "agents": [ + { + "agent_id": "a85961b5e884a9c3f", + "agent_type": "general-purpose", + "started_at": "2026-04-24T19:32:44.270Z", + "parent_mode": "none", + "status": "completed", + "completed_at": "2026-04-24T19:35:00.633Z", + "duration_ms": 136363 + }, + { + "agent_id": "ae4a7c0082c191d0c", + "agent_type": "general-purpose", + "started_at": "2026-04-24T19:32:44.339Z", + "parent_mode": "none", + "status": "completed", + "completed_at": "2026-04-24T19:34:17.959Z", + "duration_ms": 93620 + }, + { + "agent_id": "aa404682d61fd1e64", + "agent_type": "general-purpose", + "started_at": "2026-04-24T19:35:36.895Z", + "parent_mode": "none", + "status": "completed", + "completed_at": "2026-04-24T19:37:02.609Z", + "duration_ms": 85714 + }, + { + "agent_id": "a75b2cea40e5cf3b8", + "agent_type": "general-purpose", + "started_at": "2026-04-24T19:35:36.958Z", + "parent_mode": "none", + "status": "completed", + "completed_at": "2026-04-24T19:36:20.844Z", + "duration_ms": 43886 + }, + { + "agent_id": "a91e852ab81865a94", + "agent_type": "general-purpose", + "started_at": "2026-04-24T19:37:09.791Z", + "parent_mode": "none", + "status": "completed", + "completed_at": "2026-04-24T19:37:58.704Z", + "duration_ms": 48913 + }, + { + "agent_id": "af0890850bb863cd5", + "agent_type": "general-purpose", + "started_at": "2026-04-24T19:37:09.873Z", + "parent_mode": "none", + "status": "completed", + "completed_at": "2026-04-24T19:39:58.838Z", + "duration_ms": 168965 + }, + { + "agent_id": "a7a88afbb7c155ef8", + "agent_type": "general-purpose", + "started_at": "2026-04-24T19:40:45.455Z", + "parent_mode": "none", + "status": "completed", + "completed_at": "2026-04-24T20:03:31.160Z", + "duration_ms": 1365705 + }, + { + "agent_id": "ac6e0fe11e842c942", + "agent_type": "general-purpose", + "started_at": "2026-04-24T19:40:45.525Z", + "parent_mode": "none", + "status": "completed", + "completed_at": "2026-04-24T19:43:01.896Z", + "duration_ms": 136371 + }, + { + "agent_id": "aab94b981437723ec", + "agent_type": "general-purpose", + "started_at": "2026-04-24T19:45:37.727Z", + "parent_mode": "none", + "status": "completed", + "completed_at": "2026-04-24T19:52:37.964Z", + "duration_ms": 420237 + } + ], + "total_spawned": 5, + "total_completed": 9, + "total_failed": 0, + "last_updated": "2026-04-24T20:03:31.264Z" +} \ No newline at end of file diff --git a/docs/bug-bash-runtime-endpoints.md b/docs/bug-bash-runtime-endpoints.md new file mode 100644 index 000000000..32ee01dd6 --- /dev/null +++ b/docs/bug-bash-runtime-endpoints.md @@ -0,0 +1,138 @@ +# Runtime Endpoint Support — Bug Bash + +## Feature Overview + +AgentCore runtimes support multiple endpoints, each associated with a different version. This feature adds the ability +for users to register, manage, and deploy named endpoints (version aliases) for their runtimes via the CLI. + +**What are runtime endpoints?** A runtime endpoint is a named alias that points to a specific version of a deployed +runtime. For example, a user might have a runtime `my-agent` at version 5, with: + +- `prod` endpoint → version 3 (stable release) +- `staging` endpoint → version 5 (latest) +- `canary` endpoint → version 4 (testing rollback) + +This lets teams manage traffic routing and versioning without redeploying the runtime itself. + +**What was added:** + +- `agentcore add runtime-endpoint` — CLI command + TUI wizard to add an endpoint to a runtime +- `agentcore remove runtime-endpoint` — CLI command + TUI flow to remove an endpoint +- `agentcore status` — endpoints appear nested under their parent agent with deployment state +- `agentcore deploy` — endpoints are deployed as `AWS::BedrockAgentCore::RuntimeEndpoint` CloudFormation resources +- Schema: `endpoints` dictionary on each runtime in `agentcore.json` +- Deployed state: `runtimeEndpoints` section in `deployed-state.json` with endpoint IDs and ARNs + +--- + +## Test Flows + +### Flow 1: Add Runtime Endpoint (CLI) + +**Goal:** Verify the CLI command correctly adds endpoints to agentcore.json. + +| Step | Command | Expected | +| ---- | ---------------------------------------------------------------------------------------------------------------- | ---------------------------------------- | +| 1a | `agentcore add runtime-endpoint --runtime --endpoint prod --version 3 --description "Production" --json` | Success JSON, endpoint in agentcore.json | +| 1b | `agentcore add runtime-endpoint --runtime --endpoint staging --json` | Success, version defaults to 1 | +| 1c | `agentcore add runtime-endpoint --runtime NonExistent --endpoint test --json` | Error: runtime not found | +| 1d | `agentcore add runtime-endpoint --runtime --endpoint prod --json` (duplicate) | Error: already exists | +| 1e | `agentcore add runtime-endpoint --runtime --endpoint "bad name!" --json` | Error: invalid name | +| 1f | `agentcore add runtime-endpoint --runtime --endpoint test --version 0 --json` | Error: version must be >= 1 | + +### Flow 2: Add Runtime Endpoint (TUI) + +**Goal:** Verify the interactive TUI wizard works correctly. + +| Step | Action | Expected | +| ---- | ------------------------------------------------------------- | --------------------------------------------------------------- | +| 2a | `agentcore` → Add → Runtime Endpoint (single runtime project) | Skips runtime selection, goes directly to endpoint form | +| 2b | `agentcore` → Add → Runtime Endpoint (multi-runtime project) | Shows runtime picker first | +| 2c | In endpoint form, tab between fields | All 3 fields visible, active field highlighted in cyan | +| 2d | Fill name + version + description → Enter | Advances to confirm screen | +| 2e | Confirm screen shows correct values | Runtime, endpoint, version, description displayed | +| 2f | Press Enter on confirm | Success screen: "Run `agentcore deploy` to create the endpoint" | +| 2g | Press Esc on endpoint form | Goes back (to runtime picker or exits) | + +### Flow 3: Remove Runtime Endpoint (CLI) + +**Goal:** Verify the CLI command correctly removes endpoints from agentcore.json. + +| Step | Command | Expected | +| ---- | ------------------------------------------------------------- | ------------------------------------------------- | +| 3a | `agentcore remove runtime-endpoint --name prod --json` | Success, endpoint removed from agentcore.json | +| 3b | `agentcore remove runtime-endpoint --name nonexistent --json` | Error: not found | +| 3c | Verify agentcore.json after removal | Endpoint gone, other endpoints untouched | +| 3d | Remove last endpoint on a runtime | `endpoints` key removed entirely (not empty `{}`) | + +### Flow 4: Remove Runtime Endpoint (TUI) + +**Goal:** Verify the interactive remove flow works. + +| Step | Action | Expected | +| ---- | --------------------------------------- | --------------------------------------------------- | +| 4a | `agentcore` → Remove → Runtime Endpoint | Shows list of endpoints with runtime name + version | +| 4b | Select an endpoint | Shows confirmation screen with details | +| 4c | Confirm removal | Success screen with deploy reminder | +| 4d | Esc on selection | Goes back to resource type picker | + +### Flow 5: Status Display + +**Goal:** Verify endpoints appear correctly in status output. + +| Step | Command | Expected | +| ---- | ------------------------------------------ | ------------------------------------------------- | +| 5a | `agentcore status` (before deploy) | Endpoints nested under agents with `[Local only]` | +| 5b | `agentcore status --json` | `runtime-endpoint` entries in resources array | +| 5c | `agentcore status --type runtime-endpoint` | Only endpoint entries shown | +| 5d | Deployed agent + local-only agent | Correct state per endpoint after deploy | + +### Flow 6: Deploy + Verify in AWS + +**Goal:** Verify endpoints are created in AWS and tracked in deployed state. + +| Step | Action | Expected | +| ---- | ------------------------------------- | ------------------------------------------------------------- | +| 6a | `agentcore deploy` | Deploys without errors, CFN creates RuntimeEndpoint resources | +| 6b | Check CloudFormation stack | `AWS::BedrockAgentCore::RuntimeEndpoint` resources present | +| 6c | Check deployed-state.json | `runtimeEndpoints` section with endpoint IDs and ARNs | +| 6d | `agentcore status` after deploy | Endpoints show `[Deployed]` | +| 6e | AWS API: `get-agent-runtime-endpoint` | Endpoint exists with correct version and name | +| 6f | Remove endpoint + redeploy | Endpoint torn down in AWS | + +### Flow 7: Edge Cases + +**Goal:** Verify correct behavior in unusual scenarios. + +| Step | Scenario | Expected | +| ---- | -------------------------------------------------- | -------------------------------------- | +| 7a | Same endpoint name on different runtimes | Both work (namespaced by runtime) | +| 7b | Multiple endpoints on one runtime | All display correctly in status | +| 7c | Remove a runtime that has endpoints | Runtime + all its endpoints removed | +| 7d | Empty endpoints dict in agentcore.json | No errors, no endpoints shown | +| 7e | Manually edit agentcore.json with invalid endpoint | `agentcore validate` catches the error | +| 7f | Container build agent with endpoints | Works same as CodeZip | + +--- + +## Bug Tracking + +| Alias | Bug Type | Description | Screenshots | Notes | +| ----- | -------- | ----------- | ----------- | ----- | +| | | | | | +| | | | | | +| | | | | | +| | | | | | +| | | | | | +| | | | | | +| | | | | | +| | | | | | +| | | | | | +| | | | | | +| | | | | | +| | | | | | +| | | | | | +| | | | | | +| | | | | | + +**Bug Types:** `functional` | `ux` | `validation` | `display` | `deploy` | `crash` | `regression` diff --git a/docs/superpowers/plans/2026-04-24-runtime-endpoint-support.md b/docs/superpowers/plans/2026-04-24-runtime-endpoint-support.md new file mode 100644 index 000000000..8f5a557f9 --- /dev/null +++ b/docs/superpowers/plans/2026-04-24-runtime-endpoint-support.md @@ -0,0 +1,1593 @@ +# Runtime Endpoint Support Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or +> superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Allow users to register, manage, and deploy named endpoints (version aliases) for AgentCore runtimes via +`agentcore add runtime-endpoint` / `agentcore remove runtime-endpoint`, with full TUI support and CDK deployment. + +**Architecture:** Runtime endpoints are modeled as a sub-resource of runtimes (similar to how gateway targets are +sub-resources of gateways). An `endpoints` dictionary is added to `AgentEnvSpec` in the schema. The CLI primitive +follows the GatewayTargetPrimitive pattern — a sub-resource that validates its parent exists. The CDK construct creates +`CfnRuntimeEndpoint` resources for each endpoint entry. The `status` command and TUI display endpoints nested under +their parent runtime. + +**Tech Stack:** TypeScript, Zod (schema validation), Commander.js (CLI), Ink/React (TUI), AWS CDK (infrastructure) + +--- + +## File Structure + +### New Files (CLI repo: `agentcore-cli`) + +| File | Responsibility | +| --------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | +| `src/cli/primitives/RuntimeEndpointPrimitive.ts` | Primitive class: add/remove/preview/getRemovable/registerCommands for runtime endpoints | +| `src/cli/tui/screens/runtime-endpoint/AddRuntimeEndpointFlow.tsx` | TUI add flow wrapper (wizard state machine) | +| `src/cli/tui/screens/runtime-endpoint/AddRuntimeEndpointScreen.tsx` | TUI add screen (step-by-step input collection) | +| `src/cli/tui/screens/runtime-endpoint/useAddRuntimeEndpointWizard.ts` | React hook managing wizard steps and config state | +| `src/cli/tui/screens/runtime-endpoint/types.ts` | Types for the runtime endpoint wizard config | +| `src/cli/tui/screens/runtime-endpoint/index.ts` | Barrel exports | +| `src/cli/tui/screens/remove/RemoveRuntimeEndpointScreen.tsx` | TUI remove screen (list endpoints for selection) | + +### Modified Files (CLI repo: `agentcore-cli`) + +| File | Change | +| --------------------------------------------- | -------------------------------------------------------------------------------------------------------- | +| `src/schema/schemas/agent-env.ts` | Add `RuntimeEndpointSchema`, `endpoints` field to `AgentEnvSpecSchema` | +| `src/schema/schemas/deployed-state.ts` | Add `runtimeVersion` to `AgentCoreDeployedStateSchema` | +| `src/cli/primitives/registry.ts` | Add `runtimeEndpointPrimitive` singleton + to `ALL_PRIMITIVES` | +| `src/cli/primitives/index.ts` | Export `RuntimeEndpointPrimitive` | +| `src/cli/commands/remove/types.ts` | Add `'runtime-endpoint'` to `ResourceType` union | +| `src/cli/commands/status/action.ts` | Add `'runtime-endpoint'` to `ResourceStatusEntry.resourceType`, store `runtimeVersion` in deployed state | +| `src/cli/tui/screens/add/AddScreen.tsx` | Add `'runtime-endpoint'` to `ADD_RESOURCES` | +| `src/cli/tui/screens/add/AddFlow.tsx` | Add `'runtime-endpoint-wizard'` flow state + render `AddRuntimeEndpointFlow` | +| `src/cli/tui/screens/remove/RemoveFlow.tsx` | Add runtime-endpoint flow states, hooks, handlers, screens | +| `src/cli/tui/screens/remove/RemoveScreen.tsx` | Add `'runtime-endpoint'` to remove resource list | +| `src/cli/tui/hooks/useRemove.ts` | Add `useRemovableRuntimeEndpoints` and `useRemoveRuntimeEndpoint` hooks | +| `src/cli/tui/components/ResourceGraph.tsx` | Display endpoints nested under agents in status view | + +### Modified Files (CDK repo: `agentcore-l3-cdk-constructs`) + +| File | Change | +| ------------------------------------------------ | ----------------------------------------------------------------------------------- | +| `src/schema/schemas/agent-env.ts` | Add `RuntimeEndpointSchema`, `endpoints` field to `AgentEnvSpecSchema` (mirror CLI) | +| `src/cdk/constructs/l3/AgentEnvironment.ts` | Create `CfnRuntimeEndpoint` resources for each endpoint entry | +| `src/cdk/constructs/components/mcp/mcp-utils.ts` | Add `getRuntimeEndpointUrlWithQualifier()` function | + +--- + +## Task 1: Add `endpoints` field to AgentEnvSpec schema (CLI repo) + +**Files:** + +- Modify: `agentcore-cli/src/schema/schemas/agent-env.ts:195-272` +- Test: `agentcore-cli/src/schema/schemas/__tests__/agent-env.test.ts` + +- [ ] **Step 1: Write the failing test** + +In the existing agent-env test file, add tests for the new endpoints field: + +```typescript +describe('endpoints', () => { + it('accepts a valid endpoints dictionary', () => { + const spec = { + ...validCodeZipAgent, + endpoints: { + prod: { version: 3, description: 'Production traffic' }, + staging: { version: 2 }, + }, + }; + const result = AgentEnvSpecSchema.safeParse(spec); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.endpoints).toEqual({ + prod: { version: 3, description: 'Production traffic' }, + staging: { version: 2 }, + }); + } + }); + + it('defaults to undefined when endpoints is not provided', () => { + const result = AgentEnvSpecSchema.safeParse(validCodeZipAgent); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.endpoints).toBeUndefined(); + } + }); + + it('rejects endpoint name with invalid characters', () => { + const spec = { + ...validCodeZipAgent, + endpoints: { + 'invalid name!': { version: 1 }, + }, + }; + const result = AgentEnvSpecSchema.safeParse(spec); + expect(result.success).toBe(false); + }); + + it('rejects endpoint with version less than 1', () => { + const spec = { + ...validCodeZipAgent, + endpoints: { + prod: { version: 0 }, + }, + }; + const result = AgentEnvSpecSchema.safeParse(spec); + expect(result.success).toBe(false); + }); + + it('accepts endpoint with only version (description optional)', () => { + const spec = { + ...validCodeZipAgent, + endpoints: { + canary: { version: 1 }, + }, + }; + const result = AgentEnvSpecSchema.safeParse(spec); + expect(result.success).toBe(true); + }); +}); +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: +`cd /Users/tjariy/github_workspace/runtime_endpoint_project/agentcore-cli && npx vitest run src/schema/schemas/__tests__/agent-env.test.ts --reporter=verbose` +Expected: FAIL — `endpoints` field not recognized (strict schema rejects unknown keys) + +- [ ] **Step 3: Add RuntimeEndpointSchema and endpoints field to AgentEnvSpecSchema** + +In `agentcore-cli/src/schema/schemas/agent-env.ts`, add before the `AgentEnvSpecSchema` definition: + +```typescript +// ============================================================================ +// Runtime Endpoint Schema +// ============================================================================ + +/** + * Endpoint name follows the AgentCore API regex for endpoint aliases. + * https://docs.aws.amazon.com/bedrock-agentcore-control/latest/APIReference/API_CreateAgentRuntimeEndpoint.html + */ +export const RuntimeEndpointNameSchema = z + .string() + .min(1, 'Endpoint name is required') + .max(48) + .regex( + /^[a-zA-Z][a-zA-Z0-9_]{0,47}$/, + 'Must begin with a letter and contain only alphanumeric characters and underscores (max 48 chars)' + ); + +export const RuntimeEndpointSchema = z.object({ + /** Version number this endpoint points to. Must be >= 1. */ + version: z.number().int().min(1), + /** Optional human-readable description of this endpoint. */ + description: z.string().max(200).optional(), +}); + +export type RuntimeEndpoint = z.infer; +``` + +Then add the `endpoints` field to the `AgentEnvSpecSchema` object, after `filesystemConfigurations`: + +```typescript + /** Named endpoints (version aliases) for this runtime. Keys are endpoint names. */ + endpoints: z.record(RuntimeEndpointNameSchema, RuntimeEndpointSchema).optional(), +``` + +Export the new types from the file: + +```typescript +export { RuntimeEndpointNameSchema, RuntimeEndpointSchema }; +export type { RuntimeEndpoint }; +``` + +- [ ] **Step 4: Run test to verify it passes** + +Run: +`cd /Users/tjariy/github_workspace/runtime_endpoint_project/agentcore-cli && npx vitest run src/schema/schemas/__tests__/agent-env.test.ts --reporter=verbose` +Expected: PASS + +- [ ] **Step 5: Commit** + +```bash +cd /Users/tjariy/github_workspace/runtime_endpoint_project/agentcore-cli +git add src/schema/schemas/agent-env.ts src/schema/schemas/__tests__/agent-env.test.ts +git commit -m "feat: add endpoints field to AgentEnvSpec schema" +``` + +--- + +## Task 2: Add `runtimeVersion` to deployed state schema (CLI repo) + +**Files:** + +- Modify: `agentcore-cli/src/schema/schemas/deployed-state.ts:9-17` +- Test: `agentcore-cli/src/schema/schemas/__tests__/deployed-state.test.ts` + +- [ ] **Step 1: Write the failing test** + +```typescript +describe('AgentCoreDeployedStateSchema', () => { + it('accepts runtimeVersion field', () => { + const state = { + runtimeId: 'rt-123', + runtimeArn: 'arn:aws:bedrock-agentcore:us-east-1:123456789:runtime/rt-123', + roleArn: 'arn:aws:iam::123456789:role/test', + runtimeVersion: 3, + }; + const result = AgentCoreDeployedStateSchema.safeParse(state); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.runtimeVersion).toBe(3); + } + }); + + it('allows runtimeVersion to be omitted (backwards compatible)', () => { + const state = { + runtimeId: 'rt-123', + runtimeArn: 'arn:aws:bedrock-agentcore:us-east-1:123456789:runtime/rt-123', + roleArn: 'arn:aws:iam::123456789:role/test', + }; + const result = AgentCoreDeployedStateSchema.safeParse(state); + expect(result.success).toBe(true); + }); +}); +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: +`cd /Users/tjariy/github_workspace/runtime_endpoint_project/agentcore-cli && npx vitest run src/schema/schemas/__tests__/deployed-state.test.ts --reporter=verbose` +Expected: FAIL — `runtimeVersion` not in schema + +- [ ] **Step 3: Add runtimeVersion to AgentCoreDeployedStateSchema** + +In `agentcore-cli/src/schema/schemas/deployed-state.ts`, modify `AgentCoreDeployedStateSchema`: + +```typescript +export const AgentCoreDeployedStateSchema = z.object({ + runtimeId: z.string().min(1), + runtimeArn: z.string().min(1), + roleArn: z.string().min(1), + sessionId: z.string().optional(), + memoryIds: z.array(z.string()).optional(), + browserId: z.string().optional(), + codeInterpreterId: z.string().optional(), + /** The latest deployed version number of this runtime. */ + runtimeVersion: z.number().int().min(1).optional(), +}); +``` + +- [ ] **Step 4: Run test to verify it passes** + +Run: +`cd /Users/tjariy/github_workspace/runtime_endpoint_project/agentcore-cli && npx vitest run src/schema/schemas/__tests__/deployed-state.test.ts --reporter=verbose` +Expected: PASS + +- [ ] **Step 5: Commit** + +```bash +cd /Users/tjariy/github_workspace/runtime_endpoint_project/agentcore-cli +git add src/schema/schemas/deployed-state.ts src/schema/schemas/__tests__/deployed-state.test.ts +git commit -m "feat: add runtimeVersion to deployed state for version tracking" +``` + +--- + +## Task 3: Add `'runtime-endpoint'` to ResourceType union (CLI repo) + +**Files:** + +- Modify: `agentcore-cli/src/cli/commands/remove/types.ts:1-10` +- Modify: `agentcore-cli/src/cli/commands/status/action.ts:12-28` + +- [ ] **Step 1: Add 'runtime-endpoint' to ResourceType** + +In `agentcore-cli/src/cli/commands/remove/types.ts`: + +```typescript +export type ResourceType = + | 'agent' + | 'gateway' + | 'gateway-target' + | 'runtime-endpoint' + | 'memory' + | 'credential' + | 'evaluator' + | 'online-eval' + | 'policy-engine' + | 'policy'; +``` + +- [ ] **Step 2: Add 'runtime-endpoint' to ResourceStatusEntry.resourceType** + +In `agentcore-cli/src/cli/commands/status/action.ts`, update the `ResourceStatusEntry` interface: + +```typescript +export interface ResourceStatusEntry { + resourceType: + | 'agent' + | 'memory' + | 'credential' + | 'gateway' + | 'evaluator' + | 'online-eval' + | 'policy-engine' + | 'policy' + | 'runtime-endpoint'; + name: string; + deploymentState: ResourceDeploymentState; + identifier?: string; + detail?: string; + error?: string; + invocationUrl?: string; +} +``` + +- [ ] **Step 3: Commit** + +```bash +cd /Users/tjariy/github_workspace/runtime_endpoint_project/agentcore-cli +git add src/cli/commands/remove/types.ts src/cli/commands/status/action.ts +git commit -m "feat: add runtime-endpoint to ResourceType and ResourceStatusEntry" +``` + +--- + +## Task 4: Create RuntimeEndpointPrimitive (CLI repo) + +**Files:** + +- Create: `agentcore-cli/src/cli/primitives/RuntimeEndpointPrimitive.ts` +- Test: `agentcore-cli/src/cli/primitives/__tests__/RuntimeEndpointPrimitive.test.ts` + +- [ ] **Step 1: Write the failing test** + +```typescript +import type { AgentCoreProjectSpec } from '../../../schema'; +import { RuntimeEndpointPrimitive } from '../RuntimeEndpointPrimitive'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +// We'll test the primitive's core logic via its public methods +// Mock ConfigIO to avoid filesystem access +vi.mock('../../../lib', () => ({ + ConfigIO: vi.fn().mockImplementation(() => ({ + readProjectSpec: vi.fn(), + writeProjectSpec: vi.fn(), + configExists: vi.fn().mockReturnValue(false), + })), + findConfigRoot: vi.fn().mockReturnValue('/mock/agentcore'), + requireConfigRoot: vi.fn().mockReturnValue('/mock/agentcore'), +})); + +describe('RuntimeEndpointPrimitive', () => { + let primitive: RuntimeEndpointPrimitive; + + beforeEach(() => { + primitive = new RuntimeEndpointPrimitive(); + }); + + it('has kind "runtime-endpoint"', () => { + expect(primitive.kind).toBe('runtime-endpoint'); + }); + + it('has label "Runtime Endpoint"', () => { + expect(primitive.label).toBe('Runtime Endpoint'); + }); +}); +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: +`cd /Users/tjariy/github_workspace/runtime_endpoint_project/agentcore-cli && npx vitest run src/cli/primitives/__tests__/RuntimeEndpointPrimitive.test.ts --reporter=verbose` +Expected: FAIL — module not found + +- [ ] **Step 3: Create RuntimeEndpointPrimitive** + +Create `agentcore-cli/src/cli/primitives/RuntimeEndpointPrimitive.ts`: + +```typescript +import { findConfigRoot } from '../../lib'; +import type { AgentCoreProjectSpec, RuntimeEndpoint } from '../../schema'; +import { RuntimeEndpointSchema } from '../../schema'; +import type { ResourceType } from '../commands/remove/types'; +import { getErrorMessage } from '../errors'; +import type { SchemaChange } from '../operations/remove/types'; +import { BasePrimitive } from './BasePrimitive'; +import { SOURCE_CODE_NOTE } from './constants'; +import type { AddResult, AddScreenComponent, RemovableResource, RemovalPreview, RemovalResult } from './types'; +import type { Command } from '@commander-js/extra-typings'; + +/** + * Options for adding a runtime endpoint (CLI-level). + */ +export interface AddRuntimeEndpointOptions { + runtime: string; + endpoint: string; + version?: number; + description?: string; +} + +/** + * Removable runtime endpoint with parent runtime info. + */ +export interface RemovableRuntimeEndpoint extends RemovableResource { + runtimeName: string; + endpointName: string; + version: number; + description?: string; +} + +/** + * RuntimeEndpointPrimitive handles all runtime endpoint add/remove operations. + * Follows the sub-resource pattern established by GatewayTargetPrimitive. + */ +export class RuntimeEndpointPrimitive extends BasePrimitive { + readonly kind: ResourceType = 'runtime-endpoint'; + readonly label = 'Runtime Endpoint'; + readonly primitiveSchema = RuntimeEndpointSchema; + + async add(options: AddRuntimeEndpointOptions): Promise { + try { + const project = await this.readProjectSpec(); + + // Validate runtime exists + const runtime = project.runtimes.find(r => r.name === options.runtime); + if (!runtime) { + return { success: false, error: `Runtime "${options.runtime}" not found in agentcore.json.` }; + } + + // Initialize endpoints if needed + if (!runtime.endpoints) { + runtime.endpoints = {}; + } + + // Check for duplicate endpoint name + if (runtime.endpoints[options.endpoint]) { + return { + success: false, + error: `Endpoint "${options.endpoint}" already exists on runtime "${options.runtime}".`, + }; + } + + // Build endpoint config + const endpointConfig: RuntimeEndpoint = { + version: options.version ?? 1, + ...(options.description && { description: options.description }), + }; + + // Validate + RuntimeEndpointSchema.parse(endpointConfig); + + runtime.endpoints[options.endpoint] = endpointConfig; + await this.writeProjectSpec(project); + + return { success: true }; + } catch (err) { + return { success: false, error: getErrorMessage(err) }; + } + } + + async remove(name: string): Promise { + try { + const project = await this.readProjectSpec(); + + // Find the endpoint across all runtimes + for (const runtime of project.runtimes) { + if (runtime.endpoints && runtime.endpoints[name]) { + delete runtime.endpoints[name]; + // Clean up empty endpoints object + if (Object.keys(runtime.endpoints).length === 0) { + delete runtime.endpoints; + } + await this.writeProjectSpec(project); + return { success: true }; + } + } + + return { success: false, error: `Runtime endpoint "${name}" not found.` }; + } catch (err) { + return { success: false, error: getErrorMessage(err) }; + } + } + + async previewRemove(name: string): Promise { + const project = await this.readProjectSpec(); + + for (const runtime of project.runtimes) { + if (runtime.endpoints && runtime.endpoints[name]) { + const endpoint = runtime.endpoints[name]; + const summary = [ + `Removing endpoint "${name}" from runtime "${runtime.name}"`, + `Version: ${endpoint.version}`, + ...(endpoint.description ? [`Description: ${endpoint.description}`] : []), + ]; + + // Check cross-references: gateway targets that use this endpoint as a qualifier + const blockingRefs = this.findGatewayTargetReferences(project, runtime.name, name); + if (blockingRefs.length > 0) { + const refList = blockingRefs.map(ref => `"${ref.targetName}" in gateway "${ref.gatewayName}"`).join(', '); + throw new Error( + `Cannot remove endpoint "${name}" — it is referenced by gateway target ${refList}. Remove the gateway target first.` + ); + } + + const afterProject = structuredClone(project); + const afterRuntime = afterProject.runtimes.find(r => r.name === runtime.name)!; + delete afterRuntime.endpoints![name]; + if (Object.keys(afterRuntime.endpoints!).length === 0) { + delete afterRuntime.endpoints; + } + + const schemaChanges: SchemaChange[] = [ + { + file: 'agentcore/agentcore.json', + before: project, + after: afterProject, + }, + ]; + + return { summary, directoriesToDelete: [], schemaChanges }; + } + } + + throw new Error(`Runtime endpoint "${name}" not found.`); + } + + async getRemovable(): Promise { + try { + const project = await this.readProjectSpec(); + const endpoints: RemovableRuntimeEndpoint[] = []; + + for (const runtime of project.runtimes) { + if (runtime.endpoints) { + for (const [endpointName, config] of Object.entries(runtime.endpoints)) { + endpoints.push({ + name: endpointName, + type: 'runtime-endpoint', + runtimeName: runtime.name, + endpointName, + version: config.version, + description: config.description, + }); + } + } + } + + return endpoints; + } catch { + return []; + } + } + + registerCommands(addCmd: Command, removeCmd: Command): void { + addCmd + .command('runtime-endpoint') + .description('Add a named endpoint (version alias) to a runtime') + .requiredOption('--runtime ', 'Runtime to add the endpoint to') + .requiredOption('--endpoint ', 'Endpoint name') + .option('--version ', 'Version number (defaults to latest)', parseInt) + .option('--description ', 'Endpoint description') + .option('--json', 'Output as JSON [non-interactive]') + .action( + async (cliOptions: { + runtime: string; + endpoint: string; + version?: number; + description?: string; + json?: boolean; + }) => { + try { + if (!findConfigRoot()) { + console.error('No agentcore project found. Run `agentcore create` first.'); + process.exit(1); + } + + const result = await this.add({ + runtime: cliOptions.runtime, + endpoint: cliOptions.endpoint, + version: cliOptions.version, + description: cliOptions.description, + }); + + if (cliOptions.json) { + console.log(JSON.stringify(result)); + } else if (result.success) { + console.log(`Added endpoint '${cliOptions.endpoint}' to runtime '${cliOptions.runtime}'.`); + console.log('Run `agentcore deploy` to create the endpoint.'); + } else { + console.error(result.error); + } + + process.exit(result.success ? 0 : 1); + } catch (error) { + if (cliOptions.json) { + console.log(JSON.stringify({ success: false, error: getErrorMessage(error) })); + } else { + console.error(`Error: ${getErrorMessage(error)}`); + } + process.exit(1); + } + } + ); + + removeCmd + .command('runtime-endpoint') + .description('Remove a runtime endpoint from the project') + .option('--name ', 'Name of endpoint to remove [non-interactive]') + .option('-y, --yes', 'Skip confirmation prompt [non-interactive]') + .option('--json', 'Output as JSON [non-interactive]') + .action(async (cliOptions: { name?: string; yes?: boolean; json?: boolean }) => { + try { + if (!findConfigRoot()) { + console.error('No agentcore project found. Run `agentcore create` first.'); + process.exit(1); + } + + if (cliOptions.name || cliOptions.yes || cliOptions.json) { + if (!cliOptions.name) { + console.log(JSON.stringify({ success: false, error: '--name is required' })); + process.exit(1); + } + + const result = await this.remove(cliOptions.name); + console.log( + JSON.stringify({ + success: result.success, + resourceType: this.kind, + resourceName: cliOptions.name, + message: result.success ? `Removed runtime endpoint '${cliOptions.name}'` : undefined, + note: result.success ? SOURCE_CODE_NOTE : undefined, + error: !result.success ? result.error : undefined, + }) + ); + process.exit(result.success ? 0 : 1); + } else { + // TUI fallback + const [{ render }, { default: React }, { RemoveFlow }] = await Promise.all([ + import('ink'), + import('react'), + import('../tui/screens/remove'), + ]); + const { clear, unmount } = render( + React.createElement(RemoveFlow, { + isInteractive: false, + force: cliOptions.yes, + initialResourceType: this.kind, + initialResourceName: cliOptions.name, + onExit: () => { + clear(); + unmount(); + process.exit(0); + }, + }) + ); + } + } catch (error) { + if (cliOptions.json) { + console.log(JSON.stringify({ success: false, error: getErrorMessage(error) })); + } else { + console.error(`Error: ${getErrorMessage(error)}`); + } + process.exit(1); + } + }); + } + + addScreen(): AddScreenComponent { + return null; // TUI add screen is handled via AddRuntimeEndpointFlow in AddFlow.tsx + } + + /** + * Find gateway targets that reference a specific runtime endpoint as a qualifier. + * Used for cross-reference validation during remove. + */ + private findGatewayTargetReferences( + project: AgentCoreProjectSpec, + _runtimeName: string, + _endpointName: string + ): { gatewayName: string; targetName: string }[] { + // Future: when gateway targets support endpoint qualifiers, check here. + // For now, return empty — no cross-references exist yet. + return []; + } +} +``` + +- [ ] **Step 4: Run test to verify it passes** + +Run: +`cd /Users/tjariy/github_workspace/runtime_endpoint_project/agentcore-cli && npx vitest run src/cli/primitives/__tests__/RuntimeEndpointPrimitive.test.ts --reporter=verbose` +Expected: PASS + +- [ ] **Step 5: Commit** + +```bash +cd /Users/tjariy/github_workspace/runtime_endpoint_project/agentcore-cli +git add src/cli/primitives/RuntimeEndpointPrimitive.ts src/cli/primitives/__tests__/RuntimeEndpointPrimitive.test.ts +git commit -m "feat: add RuntimeEndpointPrimitive with add/remove/preview logic" +``` + +--- + +## Task 5: Register primitive in registry and exports (CLI repo) + +**Files:** + +- Modify: `agentcore-cli/src/cli/primitives/registry.ts` +- Modify: `agentcore-cli/src/cli/primitives/index.ts` + +- [ ] **Step 1: Add to registry.ts** + +Add import at top: + +```typescript +import { RuntimeEndpointPrimitive } from './RuntimeEndpointPrimitive'; +``` + +Add singleton after `policyPrimitive`: + +```typescript +export const runtimeEndpointPrimitive = new RuntimeEndpointPrimitive(); +``` + +Add to `ALL_PRIMITIVES` array (after `policyPrimitive`): + +```typescript +export const ALL_PRIMITIVES: BasePrimitive[] = [ + agentPrimitive, + memoryPrimitive, + credentialPrimitive, + evaluatorPrimitive, + onlineEvalConfigPrimitive, + gatewayPrimitive, + gatewayTargetPrimitive, + policyEnginePrimitive, + policyPrimitive, + runtimeEndpointPrimitive, +]; +``` + +- [ ] **Step 2: Add to index.ts barrel export** + +In `agentcore-cli/src/cli/primitives/index.ts`, add: + +```typescript +export { RuntimeEndpointPrimitive } from './RuntimeEndpointPrimitive'; +export type { AddRuntimeEndpointOptions, RemovableRuntimeEndpoint } from './RuntimeEndpointPrimitive'; +``` + +- [ ] **Step 3: Verify build passes** + +Run: `cd /Users/tjariy/github_workspace/runtime_endpoint_project/agentcore-cli && npx tsc --noEmit` Expected: No errors + +- [ ] **Step 4: Commit** + +```bash +cd /Users/tjariy/github_workspace/runtime_endpoint_project/agentcore-cli +git add src/cli/primitives/registry.ts src/cli/primitives/index.ts +git commit -m "feat: register RuntimeEndpointPrimitive in registry" +``` + +--- + +## Task 6: Add runtime-endpoint to TUI Add flow (CLI repo) + +**Files:** + +- Modify: `agentcore-cli/src/cli/tui/screens/add/AddScreen.tsx:4-13` +- Modify: `agentcore-cli/src/cli/tui/screens/add/AddFlow.tsx` +- Create: `agentcore-cli/src/cli/tui/screens/runtime-endpoint/types.ts` +- Create: `agentcore-cli/src/cli/tui/screens/runtime-endpoint/useAddRuntimeEndpointWizard.ts` +- Create: `agentcore-cli/src/cli/tui/screens/runtime-endpoint/AddRuntimeEndpointScreen.tsx` +- Create: `agentcore-cli/src/cli/tui/screens/runtime-endpoint/AddRuntimeEndpointFlow.tsx` +- Create: `agentcore-cli/src/cli/tui/screens/runtime-endpoint/index.ts` + +- [ ] **Step 1: Create types.ts** + +Create `agentcore-cli/src/cli/tui/screens/runtime-endpoint/types.ts`: + +```typescript +export interface RuntimeEndpointWizardConfig { + runtimeName: string; + endpointName: string; + version: number; + description?: string; +} + +export type RuntimeEndpointWizardStep = 'runtime' | 'endpoint' | 'confirm'; +``` + +- [ ] **Step 2: Create useAddRuntimeEndpointWizard.ts** + +Create `agentcore-cli/src/cli/tui/screens/runtime-endpoint/useAddRuntimeEndpointWizard.ts`: + +```typescript +import type { RuntimeEndpointWizardConfig, RuntimeEndpointWizardStep } from './types'; +import { useCallback, useMemo, useState } from 'react'; + +const BASE_STEPS: RuntimeEndpointWizardStep[] = ['runtime', 'endpoint', 'confirm']; + +export function useAddRuntimeEndpointWizard(runtimes: { name: string }[]) { + const [step, setStep] = useState(runtimes.length === 1 ? 'endpoint' : 'runtime'); + const [config, setConfig] = useState>(() => ({ + runtimeName: runtimes.length === 1 ? runtimes[0].name : undefined, + version: 1, + })); + + const steps = useMemo(() => { + if (runtimes.length === 1) { + return BASE_STEPS.filter(s => s !== 'runtime'); + } + return BASE_STEPS; + }, [runtimes.length]); + + const nextStep = useCallback( + (current: RuntimeEndpointWizardStep) => { + const idx = steps.indexOf(current); + return idx < steps.length - 1 ? steps[idx + 1] : undefined; + }, + [steps] + ); + + const goBack = useCallback(() => { + const idx = steps.indexOf(step); + if (idx > 0) { + setStep(steps[idx - 1]); + } + }, [step, steps]); + + const setRuntime = useCallback( + (name: string) => { + setConfig(c => ({ ...c, runtimeName: name })); + const next = nextStep('runtime'); + if (next) setStep(next); + }, + [nextStep] + ); + + const setEndpointDetails = useCallback( + (endpointName: string, version: number, description?: string) => { + setConfig(c => ({ ...c, endpointName, version, description })); + const next = nextStep('endpoint'); + if (next) setStep(next); + }, + [nextStep] + ); + + return { + step, + config: config as RuntimeEndpointWizardConfig, + steps, + goBack, + setRuntime, + setEndpointDetails, + }; +} +``` + +- [ ] **Step 3: Create AddRuntimeEndpointScreen.tsx** + +Create `agentcore-cli/src/cli/tui/screens/runtime-endpoint/AddRuntimeEndpointScreen.tsx`: + +```typescript +import type { AgentEnvSpec } from '../../../../schema'; +import { Panel, Screen, TextInput, WizardSelect } from '../../components'; +import { useAddRuntimeEndpointWizard } from './useAddRuntimeEndpointWizard'; +import type { RuntimeEndpointWizardConfig } from './types'; +import { Box, Text, useInput } from 'ink'; +import React, { useState } from 'react'; + +interface AddRuntimeEndpointScreenProps { + runtimes: AgentEnvSpec[]; + onComplete: (config: RuntimeEndpointWizardConfig) => void; + onBack: () => void; + onExit: () => void; +} + +export function AddRuntimeEndpointScreen({ + runtimes, + onComplete, + onBack, + onExit, +}: AddRuntimeEndpointScreenProps) { + const wizard = useAddRuntimeEndpointWizard(runtimes); + const [endpointName, setEndpointName] = useState(''); + const [version, setVersion] = useState('1'); + const [description, setDescription] = useState(''); + const [inputField, setInputField] = useState<'name' | 'version' | 'description'>('name'); + + const stepLabels = wizard.steps.map((s, i) => { + const idx = wizard.steps.indexOf(wizard.step); + const label = s === 'runtime' ? 'Runtime' : s === 'endpoint' ? 'Endpoint' : 'Confirm'; + if (i < idx) return `✓ ${label}`; + if (i === idx) return `[${i + 1} ${label}]`; + return `${i + 1} ${label}`; + }); + + const handleBack = () => { + if (wizard.steps.indexOf(wizard.step) === 0) { + onBack(); + } else { + wizard.goBack(); + } + }; + + useInput((_input, key) => { + if (key.escape) { + handleBack(); + } + }); + + return ( + + + Step: {stepLabels.join(' → ')} + + {wizard.step === 'runtime' && ( + ({ + id: r.name, + title: r.name, + description: r.build, + }))} + onSelect={item => wizard.setRuntime(item.id)} + isActive={true} + /> + )} + + {wizard.step === 'endpoint' && ( + + Runtime: {wizard.config.runtimeName} + + {inputField === 'name' && ( + + Endpoint name: + { + if (endpointName.trim()) setInputField('version'); + }} + /> + + )} + {inputField === 'version' && ( + + Endpoint name: {endpointName} + + Version: + setInputField('description')} + /> + + + )} + {inputField === 'description' && ( + + Endpoint name: {endpointName} + Version: {version} + + Description (optional): + { + wizard.setEndpointDetails( + endpointName.trim(), + parseInt(version, 10) || 1, + description.trim() || undefined + ); + }} + /> + + + )} + + + )} + + {wizard.step === 'confirm' && ( + + Review Runtime Endpoint + {'─'.repeat(25)} + Runtime: {wizard.config.runtimeName} + Endpoint: {wizard.config.endpointName} + Version: {wizard.config.version} + {wizard.config.description && ( + Description: {wizard.config.description} + )} + + ⏎ confirm esc go back + + + )} + + + + ); +} +``` + +Note: The confirm step's enter-to-submit behavior needs to be wired via `useInput` — the implementer should check the +existing patterns in `AddMemoryScreen.tsx` for the exact `useInput` + confirm pattern and replicate it. + +- [ ] **Step 4: Create AddRuntimeEndpointFlow.tsx** + +Create `agentcore-cli/src/cli/tui/screens/runtime-endpoint/AddRuntimeEndpointFlow.tsx`: + +```typescript +import { ConfigIO } from '../../../../lib'; +import type { AgentEnvSpec } from '../../../../schema'; +import { runtimeEndpointPrimitive } from '../../../primitives/registry'; +import { ErrorPrompt } from '../../components'; +import { AddSuccessScreen } from '../add/AddSuccessScreen'; +import { AddRuntimeEndpointScreen } from './AddRuntimeEndpointScreen'; +import type { RuntimeEndpointWizardConfig } from './types'; +import React, { useCallback, useEffect, useState } from 'react'; + +type FlowState = + | { name: 'loading' } + | { name: 'create-wizard'; runtimes: AgentEnvSpec[] } + | { name: 'create-success'; endpointName: string; runtimeName: string } + | { name: 'error'; message: string }; + +interface AddRuntimeEndpointFlowProps { + isInteractive: boolean; + onBack: () => void; + onExit: () => void; + onDev?: () => void; + onDeploy?: () => void; +} + +export function AddRuntimeEndpointFlow(props: AddRuntimeEndpointFlowProps) { + const [flow, setFlow] = useState({ name: 'loading' }); + + useEffect(() => { + void (async () => { + try { + const configIO = new ConfigIO(); + const project = await configIO.readProjectSpec(); + if (project.runtimes.length === 0) { + setFlow({ name: 'error', message: 'No runtimes found. Add an agent first with `agentcore add agent`.' }); + return; + } + setFlow({ name: 'create-wizard', runtimes: project.runtimes }); + } catch (err) { + setFlow({ name: 'error', message: err instanceof Error ? err.message : 'Unknown error' }); + } + })(); + }, []); + + const handleComplete = useCallback(async (config: RuntimeEndpointWizardConfig) => { + const result = await runtimeEndpointPrimitive.add({ + runtime: config.runtimeName, + endpoint: config.endpointName, + version: config.version, + description: config.description, + }); + if (result.success) { + setFlow({ name: 'create-success', endpointName: config.endpointName, runtimeName: config.runtimeName }); + } else { + setFlow({ name: 'error', message: result.error ?? 'Failed to add endpoint' }); + } + }, []); + + // Non-interactive: exit after success + useEffect(() => { + if (!props.isInteractive && flow.name === 'create-success') { + props.onExit(); + } + }, [props.isInteractive, flow.name, props.onExit]); + + if (flow.name === 'loading') { + return null; + } + + if (flow.name === 'create-wizard') { + return ( + void handleComplete(config)} + onBack={props.onBack} + onExit={props.onExit} + /> + ); + } + + if (flow.name === 'create-success') { + return ( + + ); + } + + return ( + + ); +} +``` + +- [ ] **Step 5: Create index.ts barrel** + +Create `agentcore-cli/src/cli/tui/screens/runtime-endpoint/index.ts`: + +```typescript +export { AddRuntimeEndpointFlow } from './AddRuntimeEndpointFlow'; +export { AddRuntimeEndpointScreen } from './AddRuntimeEndpointScreen'; +export type { RuntimeEndpointWizardConfig } from './types'; +``` + +- [ ] **Step 6: Add to AddScreen.tsx** + +In `agentcore-cli/src/cli/tui/screens/add/AddScreen.tsx`, add to `ADD_RESOURCES` array (after `'gateway-target'`): + +```typescript +{ id: 'runtime-endpoint', title: 'Runtime Endpoint', description: 'Named endpoint (version alias) for a runtime' }, +``` + +- [ ] **Step 7: Add to AddFlow.tsx** + +In `agentcore-cli/src/cli/tui/screens/add/AddFlow.tsx`: + +1. Add import: + +```typescript +import { AddRuntimeEndpointFlow } from '../runtime-endpoint'; +``` + +2. Add to `FlowState` union: + +```typescript +| { name: 'runtime-endpoint-wizard' } +``` + +3. Add to `getInitialFlowState`: + +```typescript +case 'runtime-endpoint': + return { name: 'runtime-endpoint-wizard' }; +``` + +4. Add to `handleSelectResource`: + +```typescript +case 'runtime-endpoint': + setFlow({ name: 'runtime-endpoint-wizard' }); + break; +``` + +5. Add render block (after the policy-wizard block): + +```typescript +if (flow.name === 'runtime-endpoint-wizard') { + return ( + setFlow({ name: 'select' })} + onDev={props.onDev} + onDeploy={props.onDeploy} + /> + ); +} +``` + +- [ ] **Step 8: Verify build passes** + +Run: `cd /Users/tjariy/github_workspace/runtime_endpoint_project/agentcore-cli && npx tsc --noEmit` Expected: No errors + +- [ ] **Step 9: Commit** + +```bash +cd /Users/tjariy/github_workspace/runtime_endpoint_project/agentcore-cli +git add src/cli/tui/screens/runtime-endpoint/ src/cli/tui/screens/add/AddScreen.tsx src/cli/tui/screens/add/AddFlow.tsx +git commit -m "feat: add runtime-endpoint TUI add flow" +``` + +--- + +## Task 7: Add runtime-endpoint to TUI Remove flow (CLI repo) + +**Files:** + +- Create: `agentcore-cli/src/cli/tui/screens/remove/RemoveRuntimeEndpointScreen.tsx` +- Modify: `agentcore-cli/src/cli/tui/screens/remove/RemoveFlow.tsx` +- Modify: `agentcore-cli/src/cli/tui/screens/remove/RemoveScreen.tsx` +- Modify: `agentcore-cli/src/cli/tui/hooks/useRemove.ts` + +- [ ] **Step 1: Add hooks to useRemove.ts** + +In `agentcore-cli/src/cli/tui/hooks/useRemove.ts`, add: + +```typescript +import type { RemovableRuntimeEndpoint } from '../../primitives/RuntimeEndpointPrimitive'; +import { runtimeEndpointPrimitive } from '../../primitives/registry'; + +export function useRemovableRuntimeEndpoints() { + const { items: endpoints, ...rest } = useRemovableResources(() => runtimeEndpointPrimitive.getRemovable()); + return { endpoints, ...rest }; +} + +export function useRemoveRuntimeEndpoint() { + return useRemoveResource( + async (name, preview) => runtimeEndpointPrimitive.remove(name), + 'runtime-endpoint', + name => name + ); +} +``` + +Also add a `loadRuntimeEndpointPreview` function to the `useRemovalPreview` hook: + +```typescript +const loadRuntimeEndpointPreview = useCallback(async (name: string) => { + try { + const preview = await runtimeEndpointPrimitive.previewRemove(name); + return { ok: true as const, preview }; + } catch (err) { + return { ok: false as const, error: err instanceof Error ? err.message : 'Unknown error' }; + } +}, []); +``` + +Return it from the hook alongside the others. + +- [ ] **Step 2: Create RemoveRuntimeEndpointScreen.tsx** + +Create `agentcore-cli/src/cli/tui/screens/remove/RemoveRuntimeEndpointScreen.tsx`: + +```typescript +import type { RemovableRuntimeEndpoint } from '../../../primitives/RuntimeEndpointPrimitive'; +import { SelectScreen } from '../../components'; +import React from 'react'; + +interface RemoveRuntimeEndpointScreenProps { + endpoints: RemovableRuntimeEndpoint[]; + onSelect: (name: string) => void; + onExit: () => void; +} + +export function RemoveRuntimeEndpointScreen({ endpoints, onSelect, onExit }: RemoveRuntimeEndpointScreenProps) { + if (endpoints.length === 0) { + return null; + } + + const items = endpoints.map(ep => ({ + id: ep.name, + title: `${ep.name}`, + description: `${ep.runtimeName} v${ep.version}${ep.description ? ` — ${ep.description}` : ''}`, + })); + + return ( + onSelect(item.id)} + onExit={onExit} + /> + ); +} +``` + +- [ ] **Step 3: Wire into RemoveFlow.tsx** + +In `agentcore-cli/src/cli/tui/screens/remove/RemoveFlow.tsx`: + +1. Add imports for the new hooks and screen. +2. Add flow states to the `FlowState` union: + +```typescript +| { name: 'select-runtime-endpoint' } +| { name: 'confirm-runtime-endpoint'; endpointName: string; preview: RemovalPreview } +| { name: 'runtime-endpoint-success'; endpointName: string; logFilePath?: string } +``` + +3. Add data hook: + `const { endpoints: runtimeEndpoints, isLoading: isLoadingRuntimeEndpoints, refresh: refreshRuntimeEndpoints } = useRemovableRuntimeEndpoints();` +4. Add to `isLoading` computation. +5. Add `handleSelectRuntimeEndpoint` callback (same pattern as others). +6. Add `handleConfirmRuntimeEndpoint` callback. +7. Add `case 'runtime-endpoint':` to `handleSelectResource` and `getInitialState`. +8. Add render blocks for `select-runtime-endpoint`, `confirm-runtime-endpoint`, and `runtime-endpoint-success`. +9. Add to `resetAll` and `refreshAll`. +10. Add to the `RemoveScreen` component's props (endpointCount). + +- [ ] **Step 4: Add to RemoveScreen.tsx** + +Add `'runtime-endpoint'` to the remove resources list with a count badge. + +- [ ] **Step 5: Verify build passes** + +Run: `cd /Users/tjariy/github_workspace/runtime_endpoint_project/agentcore-cli && npx tsc --noEmit` Expected: No errors + +- [ ] **Step 6: Commit** + +```bash +cd /Users/tjariy/github_workspace/runtime_endpoint_project/agentcore-cli +git add src/cli/tui/screens/remove/ src/cli/tui/hooks/useRemove.ts +git commit -m "feat: add runtime-endpoint TUI remove flow" +``` + +--- + +## Task 8: Display endpoints in status and ResourceGraph (CLI repo) + +**Files:** + +- Modify: `agentcore-cli/src/cli/tui/components/ResourceGraph.tsx` +- Modify: `agentcore-cli/src/cli/commands/status/action.ts` + +- [ ] **Step 1: Add endpoint display to ResourceGraph** + +In `agentcore-cli/src/cli/tui/components/ResourceGraph.tsx`, modify the Agents section to render endpoints nested under +each agent. After the `ResourceRow` for each agent, add: + +```typescript +{agent.endpoints && Object.entries(agent.endpoints).map(([epName, ep]) => ( + + {' '} + {epName} + {' '}v{ep.version} + {ep.description && {' '}{ep.description}} + +))} +``` + +- [ ] **Step 2: Update status action to include version in deployed state** + +In `agentcore-cli/src/cli/commands/status/action.ts`, in the `computeResourceStatuses` function, update the agents +section to include version info in the detail field when available: + +```typescript +const agents = diffResourceSet({ + resourceType: 'agent', + localItems: project.runtimes, + deployedRecord: resources?.runtimes ?? {}, + getIdentifier: deployed => deployed.runtimeArn, + getLocalDetail: item => { + const endpointCount = item.endpoints ? Object.keys(item.endpoints).length : 0; + return endpointCount > 0 ? `${endpointCount} endpoint${endpointCount !== 1 ? 's' : ''}` : undefined; + }, +}); +``` + +- [ ] **Step 3: Verify build passes** + +Run: `cd /Users/tjariy/github_workspace/runtime_endpoint_project/agentcore-cli && npx tsc --noEmit` Expected: No errors + +- [ ] **Step 4: Commit** + +```bash +cd /Users/tjariy/github_workspace/runtime_endpoint_project/agentcore-cli +git add src/cli/tui/components/ResourceGraph.tsx src/cli/commands/status/action.ts +git commit -m "feat: display runtime endpoints in status and resource graph" +``` + +--- + +## Task 9: Mirror schema changes in CDK repo + +**Files:** + +- Modify: `agentcore-l3-cdk-constructs/src/schema/schemas/agent-env.ts` +- Test: `agentcore-l3-cdk-constructs/test/schemas/agent-env.test.ts` + +- [ ] **Step 1: Write the failing test** + +Same test as Task 1 Step 1 but in the CDK repo's test directory. + +- [ ] **Step 2: Run test to verify it fails** + +Run: +`cd /Users/tjariy/github_workspace/runtime_endpoint_project/agentcore-l3-cdk-constructs && npx vitest run test/schemas/agent-env.test.ts --reporter=verbose` +Expected: FAIL + +- [ ] **Step 3: Add RuntimeEndpointSchema and endpoints field** + +Mirror the exact same schema changes from Task 1 Step 3 into +`agentcore-l3-cdk-constructs/src/schema/schemas/agent-env.ts`. The schemas must be identical across both repos. + +- [ ] **Step 4: Run test to verify it passes** + +Run: +`cd /Users/tjariy/github_workspace/runtime_endpoint_project/agentcore-l3-cdk-constructs && npx vitest run test/schemas/agent-env.test.ts --reporter=verbose` +Expected: PASS + +- [ ] **Step 5: Commit** + +```bash +cd /Users/tjariy/github_workspace/runtime_endpoint_project/agentcore-l3-cdk-constructs +git add src/schema/schemas/agent-env.ts test/schemas/agent-env.test.ts +git commit -m "feat: add endpoints field to AgentEnvSpec schema (CDK)" +``` + +--- + +## Task 10: Add CDK construct support for runtime endpoints + +**Files:** + +- Modify: `agentcore-l3-cdk-constructs/src/cdk/constructs/l3/AgentEnvironment.ts` +- Modify: `agentcore-l3-cdk-constructs/src/cdk/constructs/components/mcp/mcp-utils.ts` +- Test: `agentcore-l3-cdk-constructs/test/constructs/agent-environment.test.ts` + +**Important note:** There is no `CfnRuntimeEndpoint` L1 construct yet in `aws-cdk-lib`. The implementer must check +whether `aws_bedrockagentcore.CfnRuntimeEndpoint` exists in the version of `aws-cdk-lib` used (currently ^2.248.0). If +it does not exist: + +- Option A: Use `cdk.CfnResource` with resource type `AWS::BedrockAgentCore::RuntimeEndpoint` directly. +- Option B: Wait for the L1 to be generated and skip this task until then. + +The plan below assumes Option A (CfnResource) as a fallback. + +- [ ] **Step 1: Write the failing test** + +```typescript +describe('AgentEnvironment with endpoints', () => { + it('creates CfnResource for each endpoint in the spec', () => { + const stack = new cdk.Stack(); + const agent: AgentEnvSpec = { + ...validCodeZipAgent, + endpoints: { + prod: { version: 3, description: 'Production traffic' }, + staging: { version: 2 }, + }, + }; + + new AgentEnvironment(stack, 'TestEnv', { + projectName: 'TestProject', + agent, + configRoot: '/tmp/test', + }); + + const template = Template.fromStack(stack); + // Verify endpoint resources are created + template.resourceCountIs('AWS::BedrockAgentCore::RuntimeEndpoint', 2); + }); +}); +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: +`cd /Users/tjariy/github_workspace/runtime_endpoint_project/agentcore-l3-cdk-constructs && npx vitest run test/constructs/agent-environment.test.ts --reporter=verbose` +Expected: FAIL — no endpoint resources created + +- [ ] **Step 3: Add getRuntimeEndpointUrlWithQualifier to mcp-utils.ts** + +In `agentcore-l3-cdk-constructs/src/cdk/constructs/components/mcp/mcp-utils.ts`: + +```typescript +/** Runtime endpoint URL template with custom qualifier */ +export const RUNTIME_ENDPOINT_URL_WITH_QUALIFIER_TEMPLATE = + 'https://bedrock-agentcore.${AWS::Region}.${AWS::URLSuffix}/runtimes/${EncodedArn}/invocations?qualifier=${Qualifier}'; + +/** + * Generates a runtime endpoint URL with a specific qualifier (endpoint name). + */ +export function getRuntimeEndpointUrlWithQualifier(runtimeArn: string, qualifier: string): string { + const encodedArn = urlEncodeArn(runtimeArn); + return Fn.sub(RUNTIME_ENDPOINT_URL_WITH_QUALIFIER_TEMPLATE, { + EncodedArn: encodedArn, + Qualifier: qualifier, + }); +} +``` + +- [ ] **Step 4: Create endpoint resources in AgentEnvironment** + +In `agentcore-l3-cdk-constructs/src/cdk/constructs/l3/AgentEnvironment.ts`, after the runtime is created and env vars +are added, add endpoint creation: + +```typescript +// Create runtime endpoints (version aliases) +if (agent.endpoints) { + for (const [endpointName, endpointConfig] of Object.entries(agent.endpoints)) { + new cdk.CfnResource(this, `Endpoint${toPascalId(endpointName)}`, { + type: 'AWS::BedrockAgentCore::RuntimeEndpoint', + properties: { + AgentRuntimeId: this.runtime.runtimeId, + Name: endpointName, + AgentRuntimeVersion: String(endpointConfig.version), + ...(endpointConfig.description && { Description: endpointConfig.description }), + }, + }); + } +} +``` + +Add the import for `toPascalId` from `../../logical-ids` and `cdk` from `aws-cdk-lib`. + +- [ ] **Step 5: Run test to verify it passes** + +Run: +`cd /Users/tjariy/github_workspace/runtime_endpoint_project/agentcore-l3-cdk-constructs && npx vitest run test/constructs/agent-environment.test.ts --reporter=verbose` +Expected: PASS + +- [ ] **Step 6: Commit** + +```bash +cd /Users/tjariy/github_workspace/runtime_endpoint_project/agentcore-l3-cdk-constructs +git add src/cdk/constructs/l3/AgentEnvironment.ts src/cdk/constructs/components/mcp/mcp-utils.ts test/constructs/agent-environment.test.ts +git commit -m "feat: create CfnResource for runtime endpoints during CDK synth" +``` + +--- + +## Task 11: Update schema exports and run full test suite + +**Files:** + +- Modify: `agentcore-cli/src/schema/index.ts` (if not auto-exported) +- Modify: `agentcore-l3-cdk-constructs/src/schema/index.ts` (if not auto-exported) + +- [ ] **Step 1: Verify CLI schema exports RuntimeEndpoint types** + +Check `agentcore-cli/src/schema/index.ts` and ensure `RuntimeEndpoint`, `RuntimeEndpointSchema`, +`RuntimeEndpointNameSchema` are exported. Add if missing. + +- [ ] **Step 2: Verify CDK schema exports RuntimeEndpoint types** + +Check `agentcore-l3-cdk-constructs/src/schema/index.ts` (or `src/index.ts`) and ensure the same exports. Add if missing. + +- [ ] **Step 3: Run full CLI test suite** + +Run: `cd /Users/tjariy/github_workspace/runtime_endpoint_project/agentcore-cli && npm test` Expected: All tests pass. +Fix any snapshot failures with `npm run test:update-snapshots` if the asset files changed. + +- [ ] **Step 4: Run full CDK test suite** + +Run: `cd /Users/tjariy/github_workspace/runtime_endpoint_project/agentcore-l3-cdk-constructs && npm test` Expected: All +tests pass. + +- [ ] **Step 5: Commit any remaining fixes** + +```bash +cd /Users/tjariy/github_workspace/runtime_endpoint_project/agentcore-cli +git add -A && git commit -m "fix: update exports and fix test failures for runtime-endpoint feature" +cd /Users/tjariy/github_workspace/runtime_endpoint_project/agentcore-l3-cdk-constructs +git add -A && git commit -m "fix: update exports and fix test failures for runtime-endpoint feature" +``` + +--- + +## Notes for Implementer + +### Key decisions + +1. **Endpoints are a dictionary, not an array.** Keys are endpoint names, values have `version` and optional + `description`. This matches the spec: `"prod": { "version": 3, "description": "..." }`. +2. **No CfnRuntimeEndpoint L1 exists yet.** Task 10 uses `cdk.CfnResource` as a fallback. If/when the L1 is available, + switch to `bedrockagentcore.CfnRuntimeEndpoint`. +3. **Version defaults to 1** when not provided via CLI flag. The spec says "default to latest" — but "latest" requires + an API call to determine. For V1, default to 1 and document that users should check `agentcore status` for the + current version. +4. **Cross-reference validation** (gateway targets referencing endpoints) is stubbed out. The spec mentions this as + future work. The `findGatewayTargetReferences` method is ready for extension. +5. **Deploy behavior** is handled by CDK — no CLI deploy changes needed. The CDK stack reads `agentcore.json`, sees the + `endpoints` dict, and creates/updates the CFN resources. + +### Files NOT changed (intentionally) + +- `src/cli/commands/add/command.tsx` — subcommands auto-register via `primitive.registerCommands()` in `cli.ts`. +- `src/cli/commands/remove/command.tsx` — same auto-registration. +- `src/cli/cli.ts` — the `ALL_PRIMITIVES` loop handles registration automatically. +- `src/assets/cdk/` — the vended CDK project imports `@aws/agentcore-cdk` which contains the construct changes. diff --git a/src/cli/cloudformation/outputs.ts b/src/cli/cloudformation/outputs.ts index 8151fcac8..ce5a50ffc 100644 --- a/src/cli/cloudformation/outputs.ts +++ b/src/cli/cloudformation/outputs.ts @@ -6,6 +6,7 @@ import type { OnlineEvalDeployedState, PolicyDeployedState, PolicyEngineDeployedState, + RuntimeEndpointDeployedState, TargetDeployedState, } from '../../schema'; import { getCredentialProvider } from '../aws'; @@ -338,6 +339,38 @@ export function parsePolicyOutputs( return policies; } +/** + * Parse stack outputs into deployed state for runtime endpoints. + * + * Output key pattern: ApplicationEndpoint{AgentPascal}{EndpointPascal}(Id|Arn)Output{Hash} + */ +export function parseRuntimeEndpointOutputs( + outputs: StackOutputs, + endpointSpecs: { agentName: string; endpointName: string }[] +): Record { + const endpoints: Record = {}; + const outputKeys = Object.keys(outputs); + + for (const { agentName, endpointName } of endpointSpecs) { + const pascal = toPascalId('Endpoint', agentName, endpointName); + const idPrefix = `Application${pascal}IdOutput`; + const arnPrefix = `Application${pascal}ArnOutput`; + + const idKey = outputKeys.find(k => k.startsWith(idPrefix)); + const arnKey = outputKeys.find(k => k.startsWith(arnPrefix)); + + if (idKey && arnKey) { + const key = `${agentName}/${endpointName}`; + endpoints[key] = { + endpointId: outputs[idKey]!, + endpointArn: outputs[arnKey]!, + }; + } + } + + return endpoints; +} + export interface BuildDeployedStateOptions { targetName: string; stackName: string; @@ -351,6 +384,7 @@ export interface BuildDeployedStateOptions { onlineEvalConfigs?: Record; policyEngines?: Record; policies?: Record; + runtimeEndpoints?: Record; } /** @@ -370,6 +404,7 @@ export function buildDeployedState(opts: BuildDeployedStateOptions): DeployedSta onlineEvalConfigs, policyEngines, policies, + runtimeEndpoints, } = opts; const targetState: TargetDeployedState = { resources: { @@ -404,6 +439,11 @@ export function buildDeployedState(opts: BuildDeployedStateOptions): DeployedSta targetState.resources!.onlineEvalConfigs = onlineEvalConfigs; } + // Add runtime endpoint state if endpoints exist + if (runtimeEndpoints && Object.keys(runtimeEndpoints).length > 0) { + targetState.resources!.runtimeEndpoints = runtimeEndpoints; + } + return { targets: { ...existingState?.targets, diff --git a/src/cli/commands/deploy/actions.ts b/src/cli/commands/deploy/actions.ts index b3d7ad55b..ed88c1dfe 100644 --- a/src/cli/commands/deploy/actions.ts +++ b/src/cli/commands/deploy/actions.ts @@ -13,6 +13,7 @@ import { parseOnlineEvalOutputs, parsePolicyEngineOutputs, parsePolicyOutputs, + parseRuntimeEndpointOutputs, } from '../../cloudformation'; import { getErrorMessage } from '../../errors'; import { ExecLogger } from '../../logging'; @@ -403,6 +404,17 @@ export async function handleDeploy(options: ValidatedDeployOptions): Promise `${item.engineName}/${item.name}`, }); + // Flatten runtime endpoints for diffing against deployed state + const localEndpoints: { name: string; agentName: string; version: number; description?: string }[] = []; + for (const runtime of project.runtimes) { + if (runtime.endpoints) { + for (const [epName, ep] of Object.entries(runtime.endpoints)) { + localEndpoints.push({ + name: epName, + agentName: runtime.name, + version: ep.version, + description: ep.description, + }); + } + } + } + + const runtimeEndpoints = diffResourceSet({ + resourceType: 'runtime-endpoint', + localItems: localEndpoints, + deployedRecord: resources?.runtimeEndpoints ?? {}, + getIdentifier: deployed => deployed.endpointArn, + getLocalDetail: item => `${item.agentName} v${item.version}${item.description ? ` — ${item.description}` : ''}`, + getDeployedKey: item => `${item.agentName}/${item.name}`, + }); + return [ ...agents, + ...runtimeEndpoints, ...credentials, ...memories, ...gateways, diff --git a/src/cli/commands/status/command.tsx b/src/cli/commands/status/command.tsx index 9e6de5e37..0d018e58f 100644 --- a/src/cli/commands/status/command.tsx +++ b/src/cli/commands/status/command.tsx @@ -9,6 +9,7 @@ import { Box, Text, render } from 'ink'; const VALID_RESOURCE_TYPES = [ 'agent', + 'runtime-endpoint', 'memory', 'credential', 'gateway', @@ -135,6 +136,7 @@ export const registerStatus = (program: Command) => { const filtered = filterResources(result.resources, cliOptions); const agents = filtered.filter(r => r.resourceType === 'agent'); + const runtimeEndpoints = filtered.filter(r => r.resourceType === 'runtime-endpoint'); const credentials = filtered.filter(r => r.resourceType === 'credential'); const memories = filtered.filter(r => r.resourceType === 'memory'); const gateways = filtered.filter(r => r.resourceType === 'gateway'); @@ -153,16 +155,28 @@ export const registerStatus = (program: Command) => { {agents.length > 0 && ( Agents - {agents.map(entry => ( - - - {entry.invocationUrl && ( - - {' '}URL: {entry.invocationUrl} - - )} - - ))} + {agents.map(entry => { + // Find endpoints belonging to this agent + const agentEndpoints = runtimeEndpoints.filter(ep => ep.detail?.startsWith(`${entry.name} `)); + return ( + + + {entry.invocationUrl && ( + + {' '}URL: {entry.invocationUrl} + + )} + {agentEndpoints.map(ep => ( + + {' '}◉ {ep.name} {ep.detail?.replace(`${entry.name} `, '')}{' '} + + [{DEPLOYMENT_STATE_LABELS[ep.deploymentState] ?? ep.deploymentState}] + + + ))} + + ); + })} )} diff --git a/src/cli/logging/remove-logger.ts b/src/cli/logging/remove-logger.ts index 2bfffcaa4..9ec7e43ad 100644 --- a/src/cli/logging/remove-logger.ts +++ b/src/cli/logging/remove-logger.ts @@ -13,6 +13,7 @@ export interface RemoveLoggerOptions { | 'credential' | 'gateway' | 'gateway-target' + | 'runtime-endpoint' | 'evaluator' | 'online-eval' | 'policy-engine' diff --git a/src/cli/primitives/RuntimeEndpointPrimitive.ts b/src/cli/primitives/RuntimeEndpointPrimitive.ts new file mode 100644 index 000000000..8085dae1c --- /dev/null +++ b/src/cli/primitives/RuntimeEndpointPrimitive.ts @@ -0,0 +1,314 @@ +import { findConfigRoot } from '../../lib'; +import type { AgentCoreProjectSpec } from '../../schema'; +import { RuntimeEndpointSchema } from '../../schema'; +import type { ResourceType } from '../commands/remove/types'; +import { getErrorMessage } from '../errors'; +import type { RemovalPreview, RemovalResult, SchemaChange } from '../operations/remove/types'; +import { BasePrimitive } from './BasePrimitive'; +import { SOURCE_CODE_NOTE } from './constants'; +import type { AddResult, AddScreenComponent, RemovableResource } from './types'; +import type { Command } from '@commander-js/extra-typings'; + +/** + * Options for adding a runtime endpoint (CLI-level). + */ +export interface AddRuntimeEndpointOptions { + runtime: string; + endpoint: string; + version?: number; + description?: string; +} + +/** + * Represents a runtime endpoint that can be removed. + */ +export interface RemovableRuntimeEndpoint extends RemovableResource { + runtimeName: string; + endpointName: string; + version: number; + description?: string; +} + +/** + * RuntimeEndpointPrimitive handles all runtime endpoint (version alias) add/remove operations. + * Endpoints are sub-resources of runtimes, stored in the `endpoints` dictionary on each runtime. + */ +export class RuntimeEndpointPrimitive extends BasePrimitive { + readonly kind: ResourceType = 'runtime-endpoint'; + readonly label = 'Runtime Endpoint'; + readonly primitiveSchema = RuntimeEndpointSchema; + + async add(options: AddRuntimeEndpointOptions): Promise { + try { + const project = await this.readProjectSpec(); + + // Find the parent runtime + const runtime = project.runtimes.find(a => a.name === options.runtime); + if (!runtime) { + return { success: false, error: `Runtime "${options.runtime}" not found.` }; + } + + // Initialize endpoints dictionary if needed + if (!runtime.endpoints) { + runtime.endpoints = {}; + } + + // Check for duplicate endpoint name + if (runtime.endpoints[options.endpoint]) { + return { + success: false, + error: `Endpoint "${options.endpoint}" already exists on runtime "${options.runtime}".`, + }; + } + + // Build and validate the endpoint config + const config = { + version: options.version ?? 1, + ...(options.description ? { description: options.description } : {}), + }; + RuntimeEndpointSchema.parse(config); + + // Set the endpoint on the runtime + runtime.endpoints[options.endpoint] = config; + + // Write updated project spec + await this.writeProjectSpec(project); + + return { success: true }; + } catch (err) { + return { success: false, error: getErrorMessage(err) }; + } + } + + async remove(name: string): Promise { + try { + const project = await this.readProjectSpec(); + + // Find the runtime that contains this endpoint + let found = false; + for (const runtime of project.runtimes) { + if (runtime.endpoints && runtime.endpoints[name]) { + delete runtime.endpoints[name]; + + // Clean up empty endpoints dictionary + if (Object.keys(runtime.endpoints).length === 0) { + delete runtime.endpoints; + } + + found = true; + break; + } + } + + if (!found) { + return { success: false, error: `Runtime endpoint "${name}" not found.` }; + } + + await this.writeProjectSpec(project); + return { success: true }; + } catch (err) { + return { success: false, error: getErrorMessage(err) }; + } + } + + async previewRemove(name: string): Promise { + const project = await this.readProjectSpec(); + + // Find the runtime that contains this endpoint + let runtimeName: string | undefined; + let endpointConfig: { version: number; description?: string } | undefined; + + for (const runtime of project.runtimes) { + if (runtime.endpoints && runtime.endpoints[name]) { + runtimeName = runtime.name; + endpointConfig = runtime.endpoints[name]; + break; + } + } + + if (!runtimeName || !endpointConfig) { + throw new Error(`Runtime endpoint "${name}" not found.`); + } + + const summary: string[] = []; + const schemaChanges: SchemaChange[] = []; + + summary.push(`Removing runtime endpoint: ${name} (from runtime "${runtimeName}")`); + summary.push(` Version: ${endpointConfig.version}`); + if (endpointConfig.description) { + summary.push(` Description: ${endpointConfig.description}`); + } + + // Build after state + const afterProject = JSON.parse(JSON.stringify(project)) as AgentCoreProjectSpec; + const afterRuntime = afterProject.runtimes.find(a => a.name === runtimeName); + if (afterRuntime?.endpoints) { + delete afterRuntime.endpoints[name]; + if (Object.keys(afterRuntime.endpoints).length === 0) { + delete afterRuntime.endpoints; + } + } + + schemaChanges.push({ + file: 'agentcore/agentcore.json', + before: project, + after: afterProject, + }); + + return { summary, directoriesToDelete: [], schemaChanges }; + } + + async getRemovable(): Promise { + try { + const project = await this.readProjectSpec(); + const removable: RemovableRuntimeEndpoint[] = []; + + for (const runtime of project.runtimes) { + if (!runtime.endpoints) continue; + + for (const [endpointName, endpointConfig] of Object.entries(runtime.endpoints)) { + removable.push({ + name: endpointName, + type: 'runtime-endpoint', + runtimeName: runtime.name, + endpointName, + version: endpointConfig.version, + description: endpointConfig.description, + }); + } + } + + return removable; + } catch { + return []; + } + } + + registerCommands(addCmd: Command, removeCmd: Command): void { + addCmd + .command('runtime-endpoint') + .description('Add a named endpoint (version alias) to a runtime') + .requiredOption('--runtime ', 'Runtime name to add the endpoint to') + .requiredOption('--endpoint ', 'Endpoint name (e.g., prod, staging)') + .option('--version ', 'Version number to alias (default: 1)', parseInt) + .option('--description ', 'Description of the endpoint') + .option('--json', 'Output as JSON [non-interactive]') + .action( + async (cliOptions: { + runtime: string; + endpoint: string; + version?: number; + description?: string; + json?: boolean; + }) => { + try { + if (!findConfigRoot()) { + console.error('No agentcore project found. Run `agentcore create` first.'); + process.exit(1); + } + + const result = await this.add({ + runtime: cliOptions.runtime, + endpoint: cliOptions.endpoint, + version: cliOptions.version, + description: cliOptions.description, + }); + + if (cliOptions.json) { + console.log(JSON.stringify(result)); + } else if (result.success) { + console.log(`Added runtime endpoint '${cliOptions.endpoint}' to runtime '${cliOptions.runtime}'`); + } else { + console.error(result.error); + } + + process.exit(result.success ? 0 : 1); + } catch (error) { + if (cliOptions.json) { + console.log(JSON.stringify({ success: false, error: getErrorMessage(error) })); + } else { + console.error(`Error: ${getErrorMessage(error)}`); + } + process.exit(1); + } + } + ); + + removeCmd + .command('runtime-endpoint') + .description('Remove a runtime endpoint from the project') + .option('--name ', 'Name of resource to remove [non-interactive]') + .option('-y, --yes', 'Skip confirmation prompt [non-interactive]') + .option('--json', 'Output as JSON [non-interactive]') + .action(async (cliOptions: { name?: string; yes?: boolean; json?: boolean }) => { + try { + if (!findConfigRoot()) { + console.error('No agentcore project found. Run `agentcore create` first.'); + process.exit(1); + } + + if (cliOptions.name || cliOptions.yes || cliOptions.json) { + if (!cliOptions.name) { + console.log(JSON.stringify({ success: false, error: '--name is required' })); + process.exit(1); + } + + const result = await this.remove(cliOptions.name); + console.log( + JSON.stringify({ + success: result.success, + resourceType: this.kind, + resourceName: cliOptions.name, + message: result.success ? `Removed runtime endpoint '${cliOptions.name}'` : undefined, + note: result.success ? SOURCE_CODE_NOTE : undefined, + error: !result.success ? result.error : undefined, + }) + ); + process.exit(result.success ? 0 : 1); + } else { + const [{ render }, { default: React }, { RemoveFlow }] = await Promise.all([ + import('ink'), + import('react'), + import('../tui/screens/remove'), + ]); + const { clear, unmount } = render( + React.createElement(RemoveFlow, { + isInteractive: false, + force: cliOptions.yes, + initialResourceType: this.kind, + initialResourceName: cliOptions.name, + onExit: () => { + clear(); + unmount(); + process.exit(0); + }, + }) + ); + } + } catch (error) { + if (cliOptions.json) { + console.log(JSON.stringify({ success: false, error: getErrorMessage(error) })); + } else { + console.error(`Error: ${getErrorMessage(error)}`); + } + process.exit(1); + } + }); + } + + addScreen(): AddScreenComponent { + return null; + } + + /** + * Stub for future cross-reference validation. + * Checks if any gateway targets reference a given runtime endpoint. + */ + private findGatewayTargetReferences( + _project: AgentCoreProjectSpec, + _runtimeName: string, + _endpointName: string + ): { gatewayName: string; targetName: string }[] { + return []; // Future: check gateway targets that reference this endpoint + } +} diff --git a/src/cli/primitives/__tests__/RuntimeEndpointPrimitive.test.ts b/src/cli/primitives/__tests__/RuntimeEndpointPrimitive.test.ts new file mode 100644 index 000000000..e99ecf0b6 --- /dev/null +++ b/src/cli/primitives/__tests__/RuntimeEndpointPrimitive.test.ts @@ -0,0 +1,14 @@ +import { RuntimeEndpointPrimitive } from '../RuntimeEndpointPrimitive'; +import { describe, expect, it } from 'vitest'; + +describe('RuntimeEndpointPrimitive', () => { + const primitive = new RuntimeEndpointPrimitive(); + + it('has kind "runtime-endpoint"', () => { + expect(primitive.kind).toBe('runtime-endpoint'); + }); + + it('has label "Runtime Endpoint"', () => { + expect(primitive.label).toBe('Runtime Endpoint'); + }); +}); diff --git a/src/cli/primitives/index.ts b/src/cli/primitives/index.ts index 2ef948e57..2f19f9f3a 100644 --- a/src/cli/primitives/index.ts +++ b/src/cli/primitives/index.ts @@ -6,6 +6,8 @@ export { EvaluatorPrimitive } from './EvaluatorPrimitive'; export { OnlineEvalConfigPrimitive } from './OnlineEvalConfigPrimitive'; export { GatewayPrimitive } from './GatewayPrimitive'; export { GatewayTargetPrimitive } from './GatewayTargetPrimitive'; +export { RuntimeEndpointPrimitive } from './RuntimeEndpointPrimitive'; +export type { AddRuntimeEndpointOptions, RemovableRuntimeEndpoint } from './RuntimeEndpointPrimitive'; export { ALL_PRIMITIVES, agentPrimitive, @@ -15,6 +17,7 @@ export { onlineEvalConfigPrimitive, gatewayPrimitive, gatewayTargetPrimitive, + runtimeEndpointPrimitive, getPrimitive, } from './registry'; export { SOURCE_CODE_NOTE } from './constants'; diff --git a/src/cli/primitives/registry.ts b/src/cli/primitives/registry.ts index fd46a6be7..2680d1ea6 100644 --- a/src/cli/primitives/registry.ts +++ b/src/cli/primitives/registry.ts @@ -8,6 +8,7 @@ import { MemoryPrimitive } from './MemoryPrimitive'; import { OnlineEvalConfigPrimitive } from './OnlineEvalConfigPrimitive'; import { PolicyEnginePrimitive } from './PolicyEnginePrimitive'; import { PolicyPrimitive } from './PolicyPrimitive'; +import { RuntimeEndpointPrimitive } from './RuntimeEndpointPrimitive'; import type { RemovableResource } from './types'; /** @@ -22,6 +23,7 @@ export const gatewayPrimitive = new GatewayPrimitive(); export const gatewayTargetPrimitive = new GatewayTargetPrimitive(); export const policyEnginePrimitive = new PolicyEnginePrimitive(); export const policyPrimitive = new PolicyPrimitive(); +export const runtimeEndpointPrimitive = new RuntimeEndpointPrimitive(); /** * All primitives in display order. @@ -36,6 +38,7 @@ export const ALL_PRIMITIVES: BasePrimitive[] = [ gatewayTargetPrimitive, policyEnginePrimitive, policyPrimitive, + runtimeEndpointPrimitive, ]; /** diff --git a/src/cli/tui/components/ResourceGraph.tsx b/src/cli/tui/components/ResourceGraph.tsx index 26fdcbd1a..88309dd4f 100644 --- a/src/cli/tui/components/ResourceGraph.tsx +++ b/src/cli/tui/components/ResourceGraph.tsx @@ -20,6 +20,7 @@ const ICONS = { 'online-eval': '↻', 'policy-engine': '▣', policy: '▢', + 'runtime-endpoint': '◉', } as const; interface ResourceGraphProps { @@ -180,17 +181,34 @@ export function ResourceGraph({ project, mcp, agentName, resourceStatuses }: Res const runtimeStatus = rsEntry?.error ? 'error' : rsEntry?.detail; const runtimeStatusColor = rsEntry?.error ? 'red' : getStatusColor(runtimeStatus); return ( - + + + {agent.endpoints && + Object.entries(agent.endpoints).map(([epName, ep]) => { + // Endpoints inherit deployment state from parent runtime + const parentState = rsEntry?.deploymentState; + const epState = parentState === 'deployed' ? 'deployed' : 'local-only'; + const badge = getDeploymentBadge(epState); + return ( + + {' '} + {ICONS['runtime-endpoint']} {epName} + v{ep.version} + {ep.description && {ep.description}} + {badge && [{badge.text}]} + + ); + })} + ); })} diff --git a/src/cli/tui/hooks/useRemove.ts b/src/cli/tui/hooks/useRemove.ts index ad97362ac..8331d38e6 100644 --- a/src/cli/tui/hooks/useRemove.ts +++ b/src/cli/tui/hooks/useRemove.ts @@ -4,6 +4,7 @@ import type { RemovableGatewayTarget, RemovalPreview, RemovalResult } from '../. import type { RemovableCredential } from '../../primitives/CredentialPrimitive'; import type { RemovableMemory } from '../../primitives/MemoryPrimitive'; import type { RemovablePolicyResource } from '../../primitives/PolicyPrimitive'; +import type { RemovableRuntimeEndpoint } from '../../primitives/RuntimeEndpointPrimitive'; import { agentPrimitive, credentialPrimitive, @@ -14,6 +15,7 @@ import { onlineEvalConfigPrimitive, policyEnginePrimitive, policyPrimitive, + runtimeEndpointPrimitive, } from '../../primitives/registry'; import { useCallback, useEffect, useRef, useState } from 'react'; @@ -23,6 +25,7 @@ export type { RemovableCredential as RemovableIdentity, RemovableGatewayTarget, RemovablePolicyResource, + RemovableRuntimeEndpoint, }; // ============================================================================ @@ -147,6 +150,13 @@ export function useRemovablePolicies() { return { policies, ...rest }; } +export function useRemovableRuntimeEndpoints() { + const { items: endpoints, ...rest } = useRemovableResources(() => + runtimeEndpointPrimitive.getRemovable() + ); + return { endpoints, ...rest }; +} + // ============================================================================ // Preview Hook // ============================================================================ @@ -218,6 +228,10 @@ export function useRemovalPreview() { (compositeKey: string) => loadPreview(k => policyPrimitive.previewRemove(k), compositeKey), [loadPreview] ); + const loadRuntimeEndpointPreview = useCallback( + (name: string) => loadPreview(n => runtimeEndpointPrimitive.previewRemove(n), name), + [loadPreview] + ); const reset = useCallback(() => { setState({ isLoading: false, preview: null, error: null }); @@ -234,6 +248,7 @@ export function useRemovalPreview() { loadOnlineEvalPreview, loadPolicyEnginePreview, loadPolicyPreview, + loadRuntimeEndpointPreview, reset, }; } @@ -320,3 +335,11 @@ export function useRemovePolicy() { k => k ); } + +export function useRemoveRuntimeEndpoint() { + return useRemoveResource( + (name: string) => runtimeEndpointPrimitive.remove(name), + 'runtime-endpoint', + name => name + ); +} diff --git a/src/cli/tui/screens/add/AddFlow.tsx b/src/cli/tui/screens/add/AddFlow.tsx index 491d1a72b..ac2e8e40b 100644 --- a/src/cli/tui/screens/add/AddFlow.tsx +++ b/src/cli/tui/screens/add/AddFlow.tsx @@ -13,6 +13,7 @@ import { AddGatewayFlow, AddGatewayTargetFlow } from '../mcp'; import { AddMemoryFlow } from '../memory/AddMemoryFlow'; import { AddOnlineEvalFlow } from '../online-eval'; import { AddPolicyFlow } from '../policy'; +import { AddRuntimeEndpointFlow } from '../runtime-endpoint'; import type { AddResourceType } from './AddScreen'; import { AddScreen } from './AddScreen'; import { AddSuccessScreen } from './AddSuccessScreen'; @@ -30,6 +31,7 @@ type FlowState = | { name: 'evaluator-wizard' } | { name: 'online-eval-wizard' } | { name: 'policy-wizard' } + | { name: 'runtime-endpoint-wizard' } | { name: 'agent-create-success'; agentName: string; @@ -179,6 +181,8 @@ function getInitialFlowState(resource?: AddResourceType): FlowState { return { name: 'online-eval-wizard' }; case 'policy': return { name: 'policy-wizard' }; + case 'runtime-endpoint': + return { name: 'runtime-endpoint-wizard' }; default: return { name: 'select' }; } @@ -226,6 +230,9 @@ export function AddFlow(props: AddFlowProps) { case 'policy': setFlow({ name: 'policy-wizard' }); break; + case 'runtime-endpoint': + setFlow({ name: 'runtime-endpoint-wizard' }); + break; } }, []); @@ -459,6 +466,18 @@ export function AddFlow(props: AddFlowProps) { ); } + if (flow.name === 'runtime-endpoint-wizard') { + return ( + setFlow({ name: 'select' })} + onDev={props.onDev} + onDeploy={props.onDeploy} + /> + ); + } + return ( (null); @@ -215,6 +232,7 @@ export function RemoveFlow({ 'online-eval-success', 'policy-engine-success', 'policy-success', + 'runtime-endpoint-success', ]; if (successStates.includes(flow.name)) { onExit(); @@ -254,6 +272,9 @@ export function RemoveFlow({ case 'policy': setFlow({ name: 'select-policy' }); break; + case 'runtime-endpoint': + setFlow({ name: 'select-runtime-endpoint' }); + break; case 'all': setFlow({ name: 'remove-all' }); break; @@ -464,6 +485,28 @@ export function RemoveFlow({ [loadPolicyPreview, force, removePolicyOp] ); + const handleSelectRuntimeEndpoint = useCallback( + async (endpointName: string) => { + const result = await loadRuntimeEndpointPreview(endpointName); + if (result.ok) { + if (force) { + setFlow({ name: 'loading', message: `Removing runtime endpoint ${endpointName}...` }); + const removeResult = await removeRuntimeEndpointOp(endpointName, result.preview); + if (removeResult.success) { + setFlow({ name: 'runtime-endpoint-success', endpointName }); + } else { + setFlow({ name: 'error', message: removeResult.error }); + } + } else { + setFlow({ name: 'confirm-runtime-endpoint', endpointName, preview: result.preview }); + } + } else { + setFlow({ name: 'error', message: result.error }); + } + }, + [loadRuntimeEndpointPreview, force, removeRuntimeEndpointOp] + ); + // Auto-select resource when initialResourceName is provided and data is loaded useEffect(() => { if (!initialResourceName || isLoading || hasTriggeredInitialSelection.current) { @@ -500,6 +543,9 @@ export function RemoveFlow({ case 'policy': void handleSelectPolicy(initialResourceName); break; + case 'runtime-endpoint': + void handleSelectRuntimeEndpoint(initialResourceName); + break; } }, 0); }, [ @@ -514,6 +560,7 @@ export function RemoveFlow({ handleSelectOnlineEval, handleSelectPolicyEngine, handleSelectPolicy, + handleSelectRuntimeEndpoint, ]); // Confirm handlers - pass preview for logging @@ -661,6 +708,22 @@ export function RemoveFlow({ [removePolicyOp] ); + const handleConfirmRuntimeEndpoint = useCallback( + async (endpointName: string, preview: RemovalPreview) => { + pendingResultRef.current = null; + setResultReady(false); + setFlow({ name: 'loading', message: `Removing runtime endpoint ${endpointName}...` }); + const result = await removeRuntimeEndpointOp(endpointName, preview); + if (result.success) { + pendingResultRef.current = { name: 'runtime-endpoint-success', endpointName, logFilePath: result.logFilePath }; + } else { + pendingResultRef.current = { name: 'error', message: result.error }; + } + setResultReady(true); + }, + [removeRuntimeEndpointOp] + ); + const resetAll = useCallback(() => { resetPreview(); resetRemoveAgent(); @@ -672,6 +735,7 @@ export function RemoveFlow({ resetRemoveOnlineEval(); resetRemovePolicyEngine(); resetRemovePolicy(); + resetRemoveRuntimeEndpoint(); }, [ resetPreview, resetRemoveAgent, @@ -683,6 +747,7 @@ export function RemoveFlow({ resetRemoveOnlineEval, resetRemovePolicyEngine, resetRemovePolicy, + resetRemoveRuntimeEndpoint, ]); const refreshAll = useCallback(async () => { @@ -696,6 +761,7 @@ export function RemoveFlow({ refreshOnlineEvals(), refreshPolicyEngines(), refreshPolicies(), + refreshRuntimeEndpoints(), ]); }, [ refreshAgents, @@ -707,6 +773,7 @@ export function RemoveFlow({ refreshOnlineEvals, refreshPolicyEngines, refreshPolicies, + refreshRuntimeEndpoints, ]); // Select screen - wait for data to load to avoid arrow position issues @@ -727,6 +794,7 @@ export function RemoveFlow({ onlineEvalCount={onlineEvalConfigs.length} policyEngineCount={policyEngines.length} policyCount={policies.length} + runtimeEndpointCount={runtimeEndpoints.length} /> ); } @@ -861,6 +929,19 @@ export function RemoveFlow({ ); } + if (flow.name === 'select-runtime-endpoint') { + if (initialResourceName && isLoading) { + return null; + } + return ( + void handleSelectRuntimeEndpoint(name)} + onExit={() => setFlow({ name: 'select' })} + /> + ); + } + // Confirmation screens if (flow.name === 'confirm-agent') { return ( @@ -961,6 +1042,17 @@ export function RemoveFlow({ ); } + if (flow.name === 'confirm-runtime-endpoint') { + return ( + void handleConfirmRuntimeEndpoint(flow.endpointName, flow.preview)} + onCancel={() => setFlow({ name: 'select-runtime-endpoint' })} + /> + ); + } + // Success screens if (flow.name === 'agent-success') { return ( @@ -1106,6 +1198,22 @@ export function RemoveFlow({ ); } + if (flow.name === 'runtime-endpoint-success') { + return ( + { + resetAll(); + void refreshAll().then(() => setFlow({ name: 'select' })); + }} + onExit={onExit} + /> + ); + } + // Remove all screen if (flow.name === 'remove-all') { return ; diff --git a/src/cli/tui/screens/remove/RemoveRuntimeEndpointScreen.tsx b/src/cli/tui/screens/remove/RemoveRuntimeEndpointScreen.tsx new file mode 100644 index 000000000..b60b11599 --- /dev/null +++ b/src/cli/tui/screens/remove/RemoveRuntimeEndpointScreen.tsx @@ -0,0 +1,29 @@ +import type { RemovableRuntimeEndpoint } from '../../../primitives/RuntimeEndpointPrimitive'; +import { SelectScreen } from '../../components'; +import React from 'react'; + +interface RemoveRuntimeEndpointScreenProps { + /** List of runtime endpoints that can be removed */ + endpoints: RemovableRuntimeEndpoint[]; + /** Called when an endpoint is selected for removal */ + onSelect: (name: string) => void; + /** Called when user cancels */ + onExit: () => void; +} + +export function RemoveRuntimeEndpointScreen({ endpoints, onSelect, onExit }: RemoveRuntimeEndpointScreenProps) { + const items = endpoints.map(ep => ({ + id: ep.name, + title: ep.name, + description: `${ep.runtimeName} v${ep.version}${ep.description ? ` — ${ep.description}` : ''}`, + })); + + return ( + onSelect(item.id)} + onExit={onExit} + /> + ); +} diff --git a/src/cli/tui/screens/remove/RemoveScreen.tsx b/src/cli/tui/screens/remove/RemoveScreen.tsx index f562b0ff6..83488ed3a 100644 --- a/src/cli/tui/screens/remove/RemoveScreen.tsx +++ b/src/cli/tui/screens/remove/RemoveScreen.tsx @@ -12,6 +12,7 @@ const REMOVE_RESOURCES = [ { id: 'policy', title: 'Policy', description: 'Remove a policy from a policy engine' }, { id: 'gateway', title: 'Gateway', description: 'Remove a gateway' }, { id: 'gateway-target', title: 'Gateway Target', description: 'Remove a gateway target' }, + { id: 'runtime-endpoint', title: 'Runtime Endpoint', description: 'Remove a runtime endpoint' }, { id: 'all', title: 'All', description: 'Reset entire agentcore project' }, ] as const; @@ -38,6 +39,8 @@ interface RemoveScreenProps { policyEngineCount: number; /** Number of policies available for removal */ policyCount: number; + /** Number of runtime endpoints available for removal */ + runtimeEndpointCount: number; } export function RemoveScreen({ @@ -52,6 +55,7 @@ export function RemoveScreen({ onlineEvalCount, policyEngineCount, policyCount, + runtimeEndpointCount, }: RemoveScreenProps) { const items: SelectableItem[] = useMemo(() => { return REMOVE_RESOURCES.map(r => { @@ -113,6 +117,12 @@ export function RemoveScreen({ description = 'No policies to remove'; } break; + case 'runtime-endpoint': + if (runtimeEndpointCount === 0) { + disabled = true; + description = 'No runtime endpoints to remove'; + } + break; case 'all': // 'all' is always available break; @@ -130,6 +140,7 @@ export function RemoveScreen({ onlineEvalCount, policyEngineCount, policyCount, + runtimeEndpointCount, ]); const isDisabled = (item: SelectableItem) => item.disabled ?? false; diff --git a/src/cli/tui/screens/remove/__tests__/RemoveScreen.test.tsx b/src/cli/tui/screens/remove/__tests__/RemoveScreen.test.tsx index 92087fc66..16e0223e3 100644 --- a/src/cli/tui/screens/remove/__tests__/RemoveScreen.test.tsx +++ b/src/cli/tui/screens/remove/__tests__/RemoveScreen.test.tsx @@ -21,6 +21,7 @@ describe('RemoveScreen', () => { onlineEvalCount={1} policyEngineCount={1} policyCount={1} + runtimeEndpointCount={1} /> ); @@ -51,6 +52,7 @@ describe('RemoveScreen', () => { onlineEvalCount={0} policyEngineCount={0} policyCount={0} + runtimeEndpointCount={0} /> ); diff --git a/src/cli/tui/screens/remove/index.ts b/src/cli/tui/screens/remove/index.ts index 79ebfb8c1..8d77b9b10 100644 --- a/src/cli/tui/screens/remove/index.ts +++ b/src/cli/tui/screens/remove/index.ts @@ -10,5 +10,6 @@ export { RemoveMemoryScreen } from './RemoveMemoryScreen'; export { RemoveOnlineEvalScreen } from './RemoveOnlineEvalScreen'; export { RemovePolicyEngineScreen } from './RemovePolicyEngineScreen'; export { RemovePolicyScreen } from './RemovePolicyScreen'; +export { RemoveRuntimeEndpointScreen } from './RemoveRuntimeEndpointScreen'; export { RemoveScreen, type RemoveResourceType } from './RemoveScreen'; export { RemoveSuccessScreen } from './RemoveSuccessScreen'; diff --git a/src/cli/tui/screens/runtime-endpoint/AddRuntimeEndpointFlow.tsx b/src/cli/tui/screens/runtime-endpoint/AddRuntimeEndpointFlow.tsx new file mode 100644 index 000000000..06838913e --- /dev/null +++ b/src/cli/tui/screens/runtime-endpoint/AddRuntimeEndpointFlow.tsx @@ -0,0 +1,104 @@ +import { ConfigIO } from '../../../../lib'; +import { runtimeEndpointPrimitive } from '../../../primitives/registry'; +import { ErrorPrompt } from '../../components'; +import { AddSuccessScreen } from '../add/AddSuccessScreen'; +import { AddRuntimeEndpointScreen } from './AddRuntimeEndpointScreen'; +import type { RuntimeEndpointWizardConfig } from './types'; +import React, { useCallback, useEffect, useState } from 'react'; + +type FlowState = + | { name: 'loading' } + | { name: 'create-wizard'; runtimeNames: string[] } + | { name: 'create-success'; endpointName: string; runtimeName: string } + | { name: 'error'; message: string }; + +interface AddRuntimeEndpointFlowProps { + isInteractive?: boolean; + onExit: () => void; + onBack: () => void; + onDev?: () => void; + onDeploy?: () => void; +} + +export function AddRuntimeEndpointFlow({ + isInteractive = true, + onExit, + onBack, + onDev, + onDeploy, +}: AddRuntimeEndpointFlowProps) { + const [flow, setFlow] = useState({ name: 'loading' }); + + // Load runtimes on mount + useEffect(() => { + void (async () => { + try { + const configIO = new ConfigIO(); + const spec = await configIO.readProjectSpec(); + const runtimeNames = spec.runtimes.map(r => r.name); + if (runtimeNames.length === 0) { + setFlow({ name: 'error', message: 'No runtimes found. Add a runtime first with `agentcore add agent`.' }); + return; + } + setFlow({ name: 'create-wizard', runtimeNames }); + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + setFlow({ name: 'error', message }); + } + })(); + }, []); + + // In non-interactive mode, exit after success + useEffect(() => { + if (!isInteractive && flow.name === 'create-success') { + onExit(); + } + }, [isInteractive, flow.name, onExit]); + + const handleCreateComplete = useCallback((config: RuntimeEndpointWizardConfig) => { + void runtimeEndpointPrimitive + .add({ + runtime: config.runtimeName, + endpoint: config.endpointName, + version: config.version, + description: config.description, + }) + .then(result => { + if (result.success) { + setFlow({ + name: 'create-success', + endpointName: config.endpointName, + runtimeName: config.runtimeName, + }); + return; + } + setFlow({ name: 'error', message: result.error ?? 'Unknown error' }); + }); + }, []); + + if (flow.name === 'loading') { + return null; + } + + if (flow.name === 'create-wizard') { + return ( + + ); + } + + if (flow.name === 'create-success') { + return ( + + ); + } + + return ; +} diff --git a/src/cli/tui/screens/runtime-endpoint/AddRuntimeEndpointScreen.tsx b/src/cli/tui/screens/runtime-endpoint/AddRuntimeEndpointScreen.tsx new file mode 100644 index 000000000..8b0ea50ba --- /dev/null +++ b/src/cli/tui/screens/runtime-endpoint/AddRuntimeEndpointScreen.tsx @@ -0,0 +1,246 @@ +import type { SelectableItem } from '../../components'; +import { ConfirmReview, Cursor, Panel, Screen, StepIndicator, WizardSelect } from '../../components'; +import { HELP_TEXT } from '../../constants'; +import { useListNavigation } from '../../hooks'; +import type { RuntimeEndpointWizardConfig, RuntimeEndpointWizardStep } from './types'; +import { useAddRuntimeEndpointWizard } from './useAddRuntimeEndpointWizard'; +import { Box, Text, useInput } from 'ink'; +import React, { useCallback, useMemo, useState } from 'react'; + +const STEP_LABELS: Record = { + runtime: 'Runtime', + endpoint: 'Endpoint', + confirm: 'Confirm', +}; + +type EndpointField = 'name' | 'version' | 'description'; +const ENDPOINT_FIELDS: EndpointField[] = ['name', 'version', 'description']; + +interface AddRuntimeEndpointScreenProps { + onComplete: (config: RuntimeEndpointWizardConfig) => void; + onExit: () => void; + runtimeNames: string[]; +} + +export function AddRuntimeEndpointScreen({ onComplete, onExit, runtimeNames }: AddRuntimeEndpointScreenProps) { + const skipRuntimeStep = runtimeNames.length === 1; + const wizard = useAddRuntimeEndpointWizard({ skipRuntimeStep }); + + const singleRuntime = skipRuntimeStep ? runtimeNames[0]! : ''; + + // Auto-select the only runtime when skipping runtime step + const effectiveConfig = useMemo(() => { + if (skipRuntimeStep && !wizard.config.runtimeName) { + return { ...wizard.config, runtimeName: singleRuntime }; + } + return wizard.config; + }, [skipRuntimeStep, wizard.config, singleRuntime]); + + // If we skip runtime step, set it immediately on first render + React.useEffect(() => { + if (skipRuntimeStep && !wizard.config.runtimeName) { + wizard.setRuntime(singleRuntime); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const isRuntimeStep = wizard.step === 'runtime'; + const isEndpointStep = wizard.step === 'endpoint'; + const isConfirmStep = wizard.step === 'confirm'; + + // Multi-field state for endpoint step (CustomClaimForm pattern) + const [activeField, setActiveField] = useState('name'); + const [endpointName, setEndpointName] = useState(''); + const [endpointVersion, setEndpointVersion] = useState('1'); + const [endpointDescription, setEndpointDescription] = useState(''); + const [error, setError] = useState(null); + + // Runtime selection items + const runtimeItems: SelectableItem[] = useMemo( + () => runtimeNames.map(name => ({ id: name, title: name })), + [runtimeNames] + ); + + const runtimeNav = useListNavigation({ + items: runtimeItems, + onSelect: item => wizard.setRuntime(item.id), + onExit: () => onExit(), + isActive: isRuntimeStep, + }); + + // Multi-field input handler for endpoint step + useInput( + (input, key) => { + if (!isEndpointStep) return; + + if (key.escape) { + if (skipRuntimeStep) { + onExit(); + } else { + wizard.goBack(); + } + return; + } + + // Tab / Up / Down to cycle fields + if (key.tab || key.upArrow || key.downArrow) { + const idx = ENDPOINT_FIELDS.indexOf(activeField); + if (key.shift || key.upArrow) { + setActiveField(ENDPOINT_FIELDS[(idx - 1 + ENDPOINT_FIELDS.length) % ENDPOINT_FIELDS.length]!); + } else { + setActiveField(ENDPOINT_FIELDS[(idx + 1) % ENDPOINT_FIELDS.length]!); + } + setError(null); + return; + } + + // Enter: advance to next field, or submit on last field + if (key.return) { + const idx = ENDPOINT_FIELDS.indexOf(activeField); + if (idx < ENDPOINT_FIELDS.length - 1) { + // Validate current field before advancing + if (activeField === 'name') { + if (!endpointName.trim()) { + setError('Endpoint name is required'); + return; + } + if (!/^[a-zA-Z][a-zA-Z0-9_]{0,47}$/.test(endpointName.trim())) { + setError('Must begin with a letter, alphanumeric + underscores only (max 48 chars)'); + return; + } + } + if (activeField === 'version') { + const num = parseInt(endpointVersion, 10); + if (isNaN(num) || num < 1) { + setError('Version must be a positive integer'); + return; + } + } + setActiveField(ENDPOINT_FIELDS[idx + 1]!); + setError(null); + return; + } + // Last field — validate and submit + if (!endpointName.trim()) { + setError('Endpoint name is required'); + return; + } + if (!/^[a-zA-Z][a-zA-Z0-9_]{0,47}$/.test(endpointName.trim())) { + setError('Must begin with a letter, alphanumeric + underscores only (max 48 chars)'); + return; + } + const ver = parseInt(endpointVersion, 10); + if (isNaN(ver) || ver < 1) { + setError('Version must be a positive integer'); + return; + } + const desc = endpointDescription.trim() || undefined; + wizard.setEndpointDetails(endpointName.trim(), ver, desc); + return; + } + + // Text input for active field + if (activeField === 'name' || activeField === 'version' || activeField === 'description') { + if (key.backspace || key.delete) { + if (activeField === 'name') setEndpointName(v => v.slice(0, -1)); + else if (activeField === 'version') setEndpointVersion(v => v.slice(0, -1)); + else setEndpointDescription(v => v.slice(0, -1)); + setError(null); + return; + } + if (input && !key.ctrl && !key.meta) { + if (activeField === 'name') setEndpointName(v => v + input); + else if (activeField === 'version') setEndpointVersion(v => v + input); + else setEndpointDescription(v => v + input); + setError(null); + return; + } + } + }, + { isActive: isEndpointStep } + ); + + // Confirm step navigation + useListNavigation({ + items: [{ id: 'confirm', title: 'Confirm' }], + onSelect: () => onComplete(effectiveConfig), + onExit: () => wizard.goBack(), + isActive: isConfirmStep, + }); + + const helpText = isRuntimeStep + ? HELP_TEXT.NAVIGATE_SELECT + : isEndpointStep + ? 'tab/↑↓ switch fields ⏎ submit' + : HELP_TEXT.CONFIRM_CANCEL; + + const headerContent = ; + + const confirmFields = useMemo( + () => [ + { label: 'Runtime', value: effectiveConfig.runtimeName }, + { label: 'Endpoint', value: effectiveConfig.endpointName }, + { label: 'Version', value: String(effectiveConfig.version) }, + ...(effectiveConfig.description ? [{ label: 'Description', value: effectiveConfig.description }] : []), + ], + [effectiveConfig] + ); + + return ( + + + {isRuntimeStep && ( + + )} + + {isEndpointStep && ( + + Runtime: {effectiveConfig.runtimeName} + + + Endpoint name: + {activeField === 'name' && !endpointName && } + + {endpointName || e.g., prod, staging} + + {activeField === 'name' && endpointName && } + + + + Version: + {activeField === 'version' && !endpointVersion && } + + {endpointVersion || 1} + + {activeField === 'version' && endpointVersion && } + + + + Description: + {activeField === 'description' && !endpointDescription && } + + {endpointDescription || (optional)} + + {activeField === 'description' && endpointDescription && } + + + + {error && ( + + {error} + + )} + + )} + + {isConfirmStep && } + + + ); +} diff --git a/src/cli/tui/screens/runtime-endpoint/index.ts b/src/cli/tui/screens/runtime-endpoint/index.ts new file mode 100644 index 000000000..92ff091fe --- /dev/null +++ b/src/cli/tui/screens/runtime-endpoint/index.ts @@ -0,0 +1,2 @@ +export { AddRuntimeEndpointFlow } from './AddRuntimeEndpointFlow'; +export type { RuntimeEndpointWizardConfig } from './types'; diff --git a/src/cli/tui/screens/runtime-endpoint/types.ts b/src/cli/tui/screens/runtime-endpoint/types.ts new file mode 100644 index 000000000..eac8e05c3 --- /dev/null +++ b/src/cli/tui/screens/runtime-endpoint/types.ts @@ -0,0 +1,8 @@ +export interface RuntimeEndpointWizardConfig { + runtimeName: string; + endpointName: string; + version: number; + description?: string; +} + +export type RuntimeEndpointWizardStep = 'runtime' | 'endpoint' | 'confirm'; diff --git a/src/cli/tui/screens/runtime-endpoint/useAddRuntimeEndpointWizard.ts b/src/cli/tui/screens/runtime-endpoint/useAddRuntimeEndpointWizard.ts new file mode 100644 index 000000000..40b109bba --- /dev/null +++ b/src/cli/tui/screens/runtime-endpoint/useAddRuntimeEndpointWizard.ts @@ -0,0 +1,61 @@ +import type { RuntimeEndpointWizardConfig, RuntimeEndpointWizardStep } from './types'; +import { useCallback, useMemo, useState } from 'react'; + +const ALL_STEPS: RuntimeEndpointWizardStep[] = ['runtime', 'endpoint', 'confirm']; + +function getDefaultConfig(): RuntimeEndpointWizardConfig { + return { + runtimeName: '', + endpointName: '', + version: 1, + }; +} + +export function useAddRuntimeEndpointWizard(options?: { skipRuntimeStep?: boolean }) { + const [config, setConfig] = useState(getDefaultConfig); + const [step, setStep] = useState(options?.skipRuntimeStep ? 'endpoint' : 'runtime'); + + const steps = useMemo( + () => (options?.skipRuntimeStep ? ALL_STEPS.filter(s => s !== 'runtime') : ALL_STEPS), + [options?.skipRuntimeStep] + ); + + const currentIndex = steps.indexOf(step); + + const goBack = useCallback(() => { + const idx = steps.indexOf(step); + const prevStep = steps[idx - 1]; + if (prevStep) setStep(prevStep); + }, [steps, step]); + + const setRuntime = useCallback((runtimeName: string) => { + setConfig(c => ({ ...c, runtimeName })); + setStep('endpoint'); + }, []); + + const setEndpointDetails = useCallback((endpointName: string, version: number, description?: string) => { + setConfig(c => ({ + ...c, + endpointName, + version, + ...(description ? { description } : {}), + })); + setStep('confirm'); + }, []); + + const reset = useCallback(() => { + setConfig(getDefaultConfig()); + setStep(options?.skipRuntimeStep ? 'endpoint' : 'runtime'); + }, [options?.skipRuntimeStep]); + + return { + config, + step, + steps, + currentIndex, + goBack, + setRuntime, + setEndpointDetails, + reset, + }; +} diff --git a/src/schema/schemas/__tests__/agent-env.test.ts b/src/schema/schemas/__tests__/agent-env.test.ts index 81b20ef99..1b8d1b668 100644 --- a/src/schema/schemas/__tests__/agent-env.test.ts +++ b/src/schema/schemas/__tests__/agent-env.test.ts @@ -9,6 +9,8 @@ import { InstrumentationSchema, LifecycleConfigurationSchema, NetworkConfigSchema, + RuntimeEndpointNameSchema, + RuntimeEndpointSchema, } from '../agent-env.js'; import { describe, expect, it } from 'vitest'; @@ -540,3 +542,129 @@ describe('AgentEnvSpecSchema - lifecycleConfiguration', () => { } }); }); + +describe('RuntimeEndpointNameSchema', () => { + it.each(['prod', 'staging', 'myEndpoint', 'v1', 'A', 'a' + '0'.repeat(47)])( + 'accepts valid endpoint name "%s"', + name => { + expect(RuntimeEndpointNameSchema.safeParse(name).success).toBe(true); + } + ); + + it('rejects empty string', () => { + expect(RuntimeEndpointNameSchema.safeParse('').success).toBe(false); + }); + + it('rejects name starting with digit', () => { + expect(RuntimeEndpointNameSchema.safeParse('1prod').success).toBe(false); + }); + + it('rejects name with hyphens', () => { + expect(RuntimeEndpointNameSchema.safeParse('my-endpoint').success).toBe(false); + }); + + it('rejects name with special characters', () => { + expect(RuntimeEndpointNameSchema.safeParse('prod!').success).toBe(false); + expect(RuntimeEndpointNameSchema.safeParse('my@endpoint').success).toBe(false); + }); + + it('rejects name exceeding 48 chars', () => { + const name = 'A' + 'b'.repeat(48); + expect(name).toHaveLength(49); + expect(RuntimeEndpointNameSchema.safeParse(name).success).toBe(false); + }); +}); + +describe('RuntimeEndpointSchema', () => { + it('accepts endpoint with version only', () => { + const result = RuntimeEndpointSchema.safeParse({ version: 1 }); + expect(result.success).toBe(true); + }); + + it('accepts endpoint with version and description', () => { + const result = RuntimeEndpointSchema.safeParse({ version: 3, description: 'Production endpoint' }); + expect(result.success).toBe(true); + }); + + it('rejects version < 1', () => { + expect(RuntimeEndpointSchema.safeParse({ version: 0 }).success).toBe(false); + expect(RuntimeEndpointSchema.safeParse({ version: -1 }).success).toBe(false); + }); + + it('rejects non-integer version', () => { + expect(RuntimeEndpointSchema.safeParse({ version: 1.5 }).success).toBe(false); + }); + + it('rejects missing version', () => { + expect(RuntimeEndpointSchema.safeParse({}).success).toBe(false); + expect(RuntimeEndpointSchema.safeParse({ description: 'no version' }).success).toBe(false); + }); +}); + +describe('AgentEnvSpecSchema - endpoints', () => { + const validAgent = { + name: 'TestAgent', + build: 'CodeZip', + entrypoint: 'main.py', + codeLocation: 'app/TestAgent/', + runtimeVersion: 'PYTHON_3_12', + }; + + it('accepts valid endpoints dictionary', () => { + const result = AgentEnvSpecSchema.safeParse({ + ...validAgent, + endpoints: { + prod: { version: 3, description: 'Production' }, + staging: { version: 2 }, + }, + }); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.endpoints).toEqual({ + prod: { version: 3, description: 'Production' }, + staging: { version: 2 }, + }); + } + }); + + it('accepts agent without endpoints (optional)', () => { + const result = AgentEnvSpecSchema.safeParse(validAgent); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.endpoints).toBeUndefined(); + } + }); + + it('rejects invalid endpoint name with special characters', () => { + const result = AgentEnvSpecSchema.safeParse({ + ...validAgent, + endpoints: { + 'my-endpoint': { version: 1 }, + }, + }); + expect(result.success).toBe(false); + }); + + it('rejects endpoint with version < 1', () => { + const result = AgentEnvSpecSchema.safeParse({ + ...validAgent, + endpoints: { + prod: { version: 0 }, + }, + }); + expect(result.success).toBe(false); + }); + + it('accepts endpoint with only version (no description)', () => { + const result = AgentEnvSpecSchema.safeParse({ + ...validAgent, + endpoints: { + prod: { version: 5 }, + }, + }); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.endpoints!.prod).toEqual({ version: 5 }); + } + }); +}); diff --git a/src/schema/schemas/__tests__/deployed-state.test.ts b/src/schema/schemas/__tests__/deployed-state.test.ts index e468e9e85..f1791712b 100644 --- a/src/schema/schemas/__tests__/deployed-state.test.ts +++ b/src/schema/schemas/__tests__/deployed-state.test.ts @@ -50,6 +50,38 @@ describe('AgentCoreDeployedStateSchema', () => { it('rejects missing required fields', () => { expect(AgentCoreDeployedStateSchema.safeParse({ runtimeId: 'rt-123' }).success).toBe(false); }); + + it('accepts runtimeVersion when provided as a valid integer >= 1', () => { + const result = AgentCoreDeployedStateSchema.safeParse({ + runtimeId: 'rt-123', + runtimeArn: 'arn:aws:bedrock:us-east-1:123:agent-runtime/rt-123', + roleArn: 'arn:aws:iam::123:role/TestRole', + runtimeVersion: 1, + }); + expect(result.success).toBe(true); + }); + + it('accepts state without runtimeVersion (backwards compatible)', () => { + const result = AgentCoreDeployedStateSchema.safeParse({ + runtimeId: 'rt-123', + runtimeArn: 'arn:aws:bedrock:us-east-1:123:agent-runtime/rt-123', + roleArn: 'arn:aws:iam::123:role/TestRole', + }); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.runtimeVersion).toBeUndefined(); + } + }); + + it('rejects runtimeVersion of 0 (must be >= 1)', () => { + const result = AgentCoreDeployedStateSchema.safeParse({ + runtimeId: 'rt-123', + runtimeArn: 'arn:aws:bedrock:us-east-1:123:agent-runtime/rt-123', + roleArn: 'arn:aws:iam::123:role/TestRole', + runtimeVersion: 0, + }); + expect(result.success).toBe(false); + }); }); describe('MemoryDeployedStateSchema', () => { diff --git a/src/schema/schemas/agent-env.ts b/src/schema/schemas/agent-env.ts index 04348f3f1..789109a38 100644 --- a/src/schema/schemas/agent-env.ts +++ b/src/schema/schemas/agent-env.ts @@ -188,6 +188,31 @@ export const LifecycleConfigurationSchema = z }); export type LifecycleConfiguration = z.infer; +// ============================================================================ +// Runtime Endpoint Schema +// ============================================================================ + +/** + * Endpoint name follows the AgentCore API regex for endpoint aliases. + */ +export const RuntimeEndpointNameSchema = z + .string() + .min(1, 'Endpoint name is required') + .max(48) + .regex( + /^[a-zA-Z][a-zA-Z0-9_]{0,47}$/, + 'Must begin with a letter and contain only alphanumeric characters and underscores (max 48 chars)' + ); + +export const RuntimeEndpointSchema = z.object({ + /** Version number this endpoint points to. Must be >= 1. */ + version: z.number().int().min(1), + /** Optional human-readable description of this endpoint. */ + description: z.string().max(200).optional(), +}); + +export type RuntimeEndpoint = z.infer; + /** * AgentEnvSpec - represents an AgentCore Runtime. * This is a top-level resource in the schema. @@ -231,6 +256,8 @@ export const AgentEnvSpecSchema = z lifecycleConfiguration: LifecycleConfigurationSchema.optional(), /** Filesystem configurations for session-scoped persistent storage. */ filesystemConfigurations: z.array(FilesystemConfigurationSchema).optional(), + /** Named endpoints (version aliases) for this runtime. Keys are endpoint names. */ + endpoints: z.record(RuntimeEndpointNameSchema, RuntimeEndpointSchema).optional(), }) .superRefine((data, ctx) => { if (data.networkMode === 'VPC' && !data.networkConfig) { diff --git a/src/schema/schemas/deployed-state.ts b/src/schema/schemas/deployed-state.ts index 2454d6b09..6eeff21b7 100644 --- a/src/schema/schemas/deployed-state.ts +++ b/src/schema/schemas/deployed-state.ts @@ -14,6 +14,8 @@ export const AgentCoreDeployedStateSchema = z.object({ memoryIds: z.array(z.string()).optional(), browserId: z.string().optional(), codeInterpreterId: z.string().optional(), + /** The latest deployed version number of this runtime. */ + runtimeVersion: z.number().int().min(1).optional(), }); export type AgentCoreDeployedState = z.infer; @@ -168,6 +170,17 @@ export const OnlineEvalDeployedStateSchema = z.object({ export type OnlineEvalDeployedState = z.infer; +// ============================================================================ +// Runtime Endpoint Deployed State +// ============================================================================ + +export const RuntimeEndpointDeployedStateSchema = z.object({ + endpointId: z.string().min(1), + endpointArn: z.string().min(1), +}); + +export type RuntimeEndpointDeployedState = z.infer; + // ============================================================================ // Deployed Resource State // ============================================================================ @@ -182,6 +195,7 @@ export const DeployedResourceStateSchema = z.object({ onlineEvalConfigs: z.record(z.string(), OnlineEvalDeployedStateSchema).optional(), policyEngines: z.record(z.string(), PolicyEngineDeployedStateSchema).optional(), policies: z.record(z.string(), PolicyDeployedStateSchema).optional(), + runtimeEndpoints: z.record(z.string(), RuntimeEndpointDeployedStateSchema).optional(), stackName: z.string().optional(), identityKmsKeyArn: z.string().optional(), }); From 3b297730e28a7f163174cd9b767518a8230e6505 Mon Sep 17 00:00:00 2001 From: "T.J Ariyawansa" Date: Fri, 24 Apr 2026 22:47:29 -0400 Subject: [PATCH 02/18] fix: correct output key prefix for runtime endpoint parsing The CFN output keys include the AgentEnvironment construct prefix (Agent{PascalName}) which was missing from the parser pattern. --- src/cli/cloudformation/outputs.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/cli/cloudformation/outputs.ts b/src/cli/cloudformation/outputs.ts index ce5a50ffc..28ced03c1 100644 --- a/src/cli/cloudformation/outputs.ts +++ b/src/cli/cloudformation/outputs.ts @@ -342,7 +342,8 @@ export function parsePolicyOutputs( /** * Parse stack outputs into deployed state for runtime endpoints. * - * Output key pattern: ApplicationEndpoint{AgentPascal}{EndpointPascal}(Id|Arn)Output{Hash} + * Output key pattern: ApplicationAgent{AgentPascal}Endpoint{AgentPascal}{EndpointPascal}(Id|Arn)Output{Hash} + * The Agent{PascalName} prefix comes from the AgentEnvironment construct in the CDK tree. */ export function parseRuntimeEndpointOutputs( outputs: StackOutputs, @@ -352,9 +353,10 @@ export function parseRuntimeEndpointOutputs( const outputKeys = Object.keys(outputs); for (const { agentName, endpointName } of endpointSpecs) { - const pascal = toPascalId('Endpoint', agentName, endpointName); - const idPrefix = `Application${pascal}IdOutput`; - const arnPrefix = `Application${pascal}ArnOutput`; + const agentPascal = toPascalId(agentName); + const endpointPascal = toPascalId('Endpoint', agentName, endpointName); + const idPrefix = `ApplicationAgent${agentPascal}${endpointPascal}IdOutput`; + const arnPrefix = `ApplicationAgent${agentPascal}${endpointPascal}ArnOutput`; const idKey = outputKeys.find(k => k.startsWith(idPrefix)); const arnKey = outputKeys.find(k => k.startsWith(arnPrefix)); From 33b81daa6e22f719e2d4f64916af1fd9039b3502 Mon Sep 17 00:00:00 2001 From: "T.J Ariyawansa" Date: Mon, 27 Apr 2026 12:15:14 -0400 Subject: [PATCH 03/18] fix: remove .omc state files and unused useCallback import - Remove .omc/ from git tracking, add to .gitignore - Remove unused useCallback import in AddRuntimeEndpointScreen.tsx --- .gitignore | 1 + ...f1f00de3-3b7d-4c4e-ab58-812e212cf9a6.jsonl | 18 -- .omc/state/hud-stdin-cache.json | 1 - .omc/state/idle-notif-cooldown.json | 5 - .omc/state/last-tool-error.json | 7 - .omc/state/mission-state.json | 157 ------------------ .../hud-state.json | 6 - .omc/state/subagent-tracking.json | 89 ---------- .../AddRuntimeEndpointScreen.tsx | 2 +- 9 files changed, 2 insertions(+), 284 deletions(-) delete mode 100644 .omc/state/agent-replay-f1f00de3-3b7d-4c4e-ab58-812e212cf9a6.jsonl delete mode 100644 .omc/state/hud-stdin-cache.json delete mode 100644 .omc/state/idle-notif-cooldown.json delete mode 100644 .omc/state/last-tool-error.json delete mode 100644 .omc/state/mission-state.json delete mode 100644 .omc/state/sessions/f1f00de3-3b7d-4c4e-ab58-812e212cf9a6/hud-state.json delete mode 100644 .omc/state/subagent-tracking.json diff --git a/.gitignore b/.gitignore index 3b00d92ae..3643d65f2 100644 --- a/.gitignore +++ b/.gitignore @@ -67,3 +67,4 @@ ProtocolTesting/ # Auto-cloned CDK constructs (from scripts/bundle.mjs) .cdk-constructs-clone/ +.omc/ diff --git a/.omc/state/agent-replay-f1f00de3-3b7d-4c4e-ab58-812e212cf9a6.jsonl b/.omc/state/agent-replay-f1f00de3-3b7d-4c4e-ab58-812e212cf9a6.jsonl deleted file mode 100644 index 224d6e448..000000000 --- a/.omc/state/agent-replay-f1f00de3-3b7d-4c4e-ab58-812e212cf9a6.jsonl +++ /dev/null @@ -1,18 +0,0 @@ -{"t":0,"agent":"a85961b","agent_type":"general-purpose","event":"agent_start","parent_mode":"none"} -{"t":0,"agent":"ae4a7c0","agent_type":"general-purpose","event":"agent_start","parent_mode":"none"} -{"t":0,"agent":"ae4a7c0","agent_type":"general-purpose","event":"agent_stop","success":true,"duration_ms":93620} -{"t":0,"agent":"a85961b","agent_type":"general-purpose","event":"agent_stop","success":true,"duration_ms":136363} -{"t":0,"agent":"aa40468","agent_type":"general-purpose","event":"agent_start","parent_mode":"none"} -{"t":0,"agent":"a75b2ce","agent_type":"general-purpose","event":"agent_start","parent_mode":"none"} -{"t":0,"agent":"a75b2ce","agent_type":"general-purpose","event":"agent_stop","success":true,"duration_ms":43886} -{"t":0,"agent":"aa40468","agent_type":"general-purpose","event":"agent_stop","success":true,"duration_ms":85714} -{"t":0,"agent":"a91e852","agent_type":"general-purpose","event":"agent_start","parent_mode":"none"} -{"t":0,"agent":"af08908","agent_type":"general-purpose","event":"agent_start","parent_mode":"none"} -{"t":0,"agent":"a91e852","agent_type":"general-purpose","event":"agent_stop","success":true,"duration_ms":48913} -{"t":0,"agent":"af08908","agent_type":"general-purpose","event":"agent_stop","success":true,"duration_ms":168965} -{"t":0,"agent":"a7a88af","agent_type":"general-purpose","event":"agent_start","parent_mode":"none"} -{"t":0,"agent":"ac6e0fe","agent_type":"general-purpose","event":"agent_start","parent_mode":"none"} -{"t":0,"agent":"ac6e0fe","agent_type":"general-purpose","event":"agent_stop","success":true,"duration_ms":136371} -{"t":0,"agent":"aab94b9","agent_type":"general-purpose","event":"agent_start","parent_mode":"none"} -{"t":0,"agent":"aab94b9","agent_type":"general-purpose","event":"agent_stop","success":true,"duration_ms":420237} -{"t":0,"agent":"a7a88af","agent_type":"general-purpose","event":"agent_stop","success":true,"duration_ms":1365705} diff --git a/.omc/state/hud-stdin-cache.json b/.omc/state/hud-stdin-cache.json deleted file mode 100644 index 02c439e7a..000000000 --- a/.omc/state/hud-stdin-cache.json +++ /dev/null @@ -1 +0,0 @@ -{"session_id":"f1f00de3-3b7d-4c4e-ab58-812e212cf9a6","transcript_path":"/Users/tjariy/.claude/projects/-Users-tjariy-github-workspace-runtime-endpoint-project/f1f00de3-3b7d-4c4e-ab58-812e212cf9a6.jsonl","cwd":"/Users/tjariy/github_workspace/runtime_endpoint_project/agentcore-cli","model":{"id":"us.anthropic.claude-opus-4-6-v1[1m]","display_name":"Opus 4.6 (1M context)"},"workspace":{"current_dir":"/Users/tjariy/github_workspace/runtime_endpoint_project/agentcore-cli","project_dir":"/Users/tjariy/github_workspace/runtime_endpoint_project","added_dirs":[]},"version":"2.1.92","output_style":{"name":"default"},"cost":{"total_cost_usd":91.16863265000003,"total_duration_ms":27828835,"total_api_duration_ms":4169427,"total_lines_added":3308,"total_lines_removed":156},"context_window":{"total_input_tokens":9962,"total_output_tokens":202304,"context_window_size":1000000,"current_usage":{"input_tokens":3,"output_tokens":150,"cache_creation_input_tokens":3313,"cache_read_input_tokens":377703},"used_percentage":38,"remaining_percentage":62},"exceeds_200k_tokens":true} \ No newline at end of file diff --git a/.omc/state/idle-notif-cooldown.json b/.omc/state/idle-notif-cooldown.json deleted file mode 100644 index db471b019..000000000 --- a/.omc/state/idle-notif-cooldown.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "lastSentAt": "2026-04-24T22:12:31.660Z", - "repoSignature": "{\"repo\":\"aws/agentcore-cli\",\"headSha\":\"3d2d671b9513df7e3e31857bc246dbc11b02bcd9\",\"dirty\":true,\"openPrNumbers\":[580,630,638,650,660,693,718,726,727,740,752,776,794,802,807,855,862,868,876,912,913,914,915,916,917,926,950,952,953,954,955,959,960,961,962,963,964,965,966],\"openIssueNumbers\":[29,38,85,140,155,174,204,205,207,234,243,245,258,274,503,542,565,570,602,603,604,607,624,629,637,659,665,666,667,668,669,670,671,672,673,674,676,679,681,682,683,684,686,699,719,737,741,746,747,761,781,800,808,809,810,815,821,838,839,840,870,889,895,900,902,906,907,927,931,932,948],\"failingRunIds\":[24910302672,24910302910,24910304889,24910354876,24910355028,24910355044,24911643922,24911643929,24911914713,24912214011,24913252566]}", - "backlogZero": false -} \ No newline at end of file diff --git a/.omc/state/last-tool-error.json b/.omc/state/last-tool-error.json deleted file mode 100644 index 112e57c44..000000000 --- a/.omc/state/last-tool-error.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "tool_name": "Read", - "tool_input_preview": "{\"file_path\":\"/Users/tjariy/github_workspace/runtime_endpoint_project/agentcore-cli/src/schema/agent-env.ts\"}", - "error": "File does not exist. Note: your current working directory is /Users/tjariy/github_workspace/runtime_endpoint_project/agentcore-cli.", - "timestamp": "2026-04-24T19:40:50.234Z", - "retry_count": 1 -} \ No newline at end of file diff --git a/.omc/state/mission-state.json b/.omc/state/mission-state.json deleted file mode 100644 index a616d5c1e..000000000 --- a/.omc/state/mission-state.json +++ /dev/null @@ -1,157 +0,0 @@ -{ - "updatedAt": "2026-04-24T20:03:31.160Z", - "missions": [ - { - "id": "session:f1f00de3-3b7d-4c4e-ab58-812e212cf9a6:none", - "source": "session", - "name": "none", - "objective": "Session mission", - "createdAt": "2026-04-24T19:32:44.270Z", - "updatedAt": "2026-04-24T20:03:31.160Z", - "status": "done", - "workerCount": 9, - "taskCounts": { - "total": 9, - "pending": 0, - "blocked": 0, - "inProgress": 0, - "completed": 9, - "failed": 0 - }, - "agents": [ - { - "name": "general-purpose:a85961b", - "role": "general-purpose", - "ownership": "a85961b5e884a9c3f", - "status": "done", - "currentStep": null, - "latestUpdate": "completed", - "completedSummary": null, - "updatedAt": "2026-04-24T19:35:00.633Z" - }, - { - "name": "general-purpose:ae4a7c0", - "role": "general-purpose", - "ownership": "ae4a7c0082c191d0c", - "status": "done", - "currentStep": null, - "latestUpdate": "completed", - "completedSummary": null, - "updatedAt": "2026-04-24T19:34:17.959Z" - }, - { - "name": "general-purpose:aa40468", - "role": "general-purpose", - "ownership": "aa404682d61fd1e64", - "status": "done", - "currentStep": null, - "latestUpdate": "completed", - "completedSummary": null, - "updatedAt": "2026-04-24T19:37:02.609Z" - }, - { - "name": "general-purpose:a75b2ce", - "role": "general-purpose", - "ownership": "a75b2cea40e5cf3b8", - "status": "done", - "currentStep": null, - "latestUpdate": "completed", - "completedSummary": null, - "updatedAt": "2026-04-24T19:36:20.844Z" - }, - { - "name": "general-purpose:a91e852", - "role": "general-purpose", - "ownership": "a91e852ab81865a94", - "status": "done", - "currentStep": null, - "latestUpdate": "completed", - "completedSummary": null, - "updatedAt": "2026-04-24T19:37:58.704Z" - }, - { - "name": "general-purpose:af08908", - "role": "general-purpose", - "ownership": "af0890850bb863cd5", - "status": "done", - "currentStep": null, - "latestUpdate": "completed", - "completedSummary": null, - "updatedAt": "2026-04-24T19:39:58.838Z" - }, - { - "name": "general-purpose:a7a88af", - "role": "general-purpose", - "ownership": "a7a88afbb7c155ef8", - "status": "done", - "currentStep": null, - "latestUpdate": "completed", - "completedSummary": null, - "updatedAt": "2026-04-24T20:03:31.160Z" - }, - { - "name": "general-purpose:ac6e0fe", - "role": "general-purpose", - "ownership": "ac6e0fe11e842c942", - "status": "done", - "currentStep": null, - "latestUpdate": "completed", - "completedSummary": null, - "updatedAt": "2026-04-24T19:43:01.896Z" - }, - { - "name": "general-purpose:aab94b9", - "role": "general-purpose", - "ownership": "aab94b981437723ec", - "status": "done", - "currentStep": null, - "latestUpdate": "completed", - "completedSummary": null, - "updatedAt": "2026-04-24T19:52:37.964Z" - } - ], - "timeline": [ - { - "id": "session-start:ac6e0fe11e842c942:2026-04-24T19:40:45.525Z", - "at": "2026-04-24T19:40:45.525Z", - "kind": "update", - "agent": "general-purpose:ac6e0fe", - "detail": "started general-purpose:ac6e0fe", - "sourceKey": "session-start:ac6e0fe11e842c942" - }, - { - "id": "session-stop:ac6e0fe11e842c942:2026-04-24T19:43:01.896Z", - "at": "2026-04-24T19:43:01.896Z", - "kind": "completion", - "agent": "general-purpose:ac6e0fe", - "detail": "completed", - "sourceKey": "session-stop:ac6e0fe11e842c942" - }, - { - "id": "session-start:aab94b981437723ec:2026-04-24T19:45:37.727Z", - "at": "2026-04-24T19:45:37.727Z", - "kind": "update", - "agent": "general-purpose:aab94b9", - "detail": "started general-purpose:aab94b9", - "sourceKey": "session-start:aab94b981437723ec" - }, - { - "id": "session-stop:aab94b981437723ec:2026-04-24T19:52:37.964Z", - "at": "2026-04-24T19:52:37.964Z", - "kind": "completion", - "agent": "general-purpose:aab94b9", - "detail": "completed", - "sourceKey": "session-stop:aab94b981437723ec" - }, - { - "id": "session-stop:a7a88afbb7c155ef8:2026-04-24T20:03:31.160Z", - "at": "2026-04-24T20:03:31.160Z", - "kind": "completion", - "agent": "general-purpose:a7a88af", - "detail": "completed", - "sourceKey": "session-stop:a7a88afbb7c155ef8" - } - ] - } - ] -} \ No newline at end of file diff --git a/.omc/state/sessions/f1f00de3-3b7d-4c4e-ab58-812e212cf9a6/hud-state.json b/.omc/state/sessions/f1f00de3-3b7d-4c4e-ab58-812e212cf9a6/hud-state.json deleted file mode 100644 index 85fe1d642..000000000 --- a/.omc/state/sessions/f1f00de3-3b7d-4c4e-ab58-812e212cf9a6/hud-state.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "timestamp": "2026-04-24T19:31:15.877Z", - "backgroundTasks": [], - "sessionStartTimestamp": "2026-04-24T18:50:17.974Z", - "sessionId": "f1f00de3-3b7d-4c4e-ab58-812e212cf9a6" -} \ No newline at end of file diff --git a/.omc/state/subagent-tracking.json b/.omc/state/subagent-tracking.json deleted file mode 100644 index e0e377e35..000000000 --- a/.omc/state/subagent-tracking.json +++ /dev/null @@ -1,89 +0,0 @@ -{ - "agents": [ - { - "agent_id": "a85961b5e884a9c3f", - "agent_type": "general-purpose", - "started_at": "2026-04-24T19:32:44.270Z", - "parent_mode": "none", - "status": "completed", - "completed_at": "2026-04-24T19:35:00.633Z", - "duration_ms": 136363 - }, - { - "agent_id": "ae4a7c0082c191d0c", - "agent_type": "general-purpose", - "started_at": "2026-04-24T19:32:44.339Z", - "parent_mode": "none", - "status": "completed", - "completed_at": "2026-04-24T19:34:17.959Z", - "duration_ms": 93620 - }, - { - "agent_id": "aa404682d61fd1e64", - "agent_type": "general-purpose", - "started_at": "2026-04-24T19:35:36.895Z", - "parent_mode": "none", - "status": "completed", - "completed_at": "2026-04-24T19:37:02.609Z", - "duration_ms": 85714 - }, - { - "agent_id": "a75b2cea40e5cf3b8", - "agent_type": "general-purpose", - "started_at": "2026-04-24T19:35:36.958Z", - "parent_mode": "none", - "status": "completed", - "completed_at": "2026-04-24T19:36:20.844Z", - "duration_ms": 43886 - }, - { - "agent_id": "a91e852ab81865a94", - "agent_type": "general-purpose", - "started_at": "2026-04-24T19:37:09.791Z", - "parent_mode": "none", - "status": "completed", - "completed_at": "2026-04-24T19:37:58.704Z", - "duration_ms": 48913 - }, - { - "agent_id": "af0890850bb863cd5", - "agent_type": "general-purpose", - "started_at": "2026-04-24T19:37:09.873Z", - "parent_mode": "none", - "status": "completed", - "completed_at": "2026-04-24T19:39:58.838Z", - "duration_ms": 168965 - }, - { - "agent_id": "a7a88afbb7c155ef8", - "agent_type": "general-purpose", - "started_at": "2026-04-24T19:40:45.455Z", - "parent_mode": "none", - "status": "completed", - "completed_at": "2026-04-24T20:03:31.160Z", - "duration_ms": 1365705 - }, - { - "agent_id": "ac6e0fe11e842c942", - "agent_type": "general-purpose", - "started_at": "2026-04-24T19:40:45.525Z", - "parent_mode": "none", - "status": "completed", - "completed_at": "2026-04-24T19:43:01.896Z", - "duration_ms": 136371 - }, - { - "agent_id": "aab94b981437723ec", - "agent_type": "general-purpose", - "started_at": "2026-04-24T19:45:37.727Z", - "parent_mode": "none", - "status": "completed", - "completed_at": "2026-04-24T19:52:37.964Z", - "duration_ms": 420237 - } - ], - "total_spawned": 5, - "total_completed": 9, - "total_failed": 0, - "last_updated": "2026-04-24T20:03:31.264Z" -} \ No newline at end of file diff --git a/src/cli/tui/screens/runtime-endpoint/AddRuntimeEndpointScreen.tsx b/src/cli/tui/screens/runtime-endpoint/AddRuntimeEndpointScreen.tsx index 8b0ea50ba..489b609a9 100644 --- a/src/cli/tui/screens/runtime-endpoint/AddRuntimeEndpointScreen.tsx +++ b/src/cli/tui/screens/runtime-endpoint/AddRuntimeEndpointScreen.tsx @@ -5,7 +5,7 @@ import { useListNavigation } from '../../hooks'; import type { RuntimeEndpointWizardConfig, RuntimeEndpointWizardStep } from './types'; import { useAddRuntimeEndpointWizard } from './useAddRuntimeEndpointWizard'; import { Box, Text, useInput } from 'ink'; -import React, { useCallback, useMemo, useState } from 'react'; +import React, { useMemo, useState } from 'react'; const STEP_LABELS: Record = { runtime: 'Runtime', From 6b4f8f39709649ddc1ec34be049cadb52c7e1a42 Mon Sep 17 00:00:00 2001 From: "T.J Ariyawansa" Date: Mon, 27 Apr 2026 15:33:33 -0400 Subject: [PATCH 04/18] fix: shorten runtime endpoint description to prevent TUI overflow The description "Named endpoint (version alias) for a runtime" was too long and wrapped to the next line in the Add Resource menu. Shortened to "Named endpoint for a runtime". --- src/cli/tui/screens/add/AddScreen.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli/tui/screens/add/AddScreen.tsx b/src/cli/tui/screens/add/AddScreen.tsx index 96271b143..7ea889660 100644 --- a/src/cli/tui/screens/add/AddScreen.tsx +++ b/src/cli/tui/screens/add/AddScreen.tsx @@ -9,7 +9,7 @@ const ADD_RESOURCES = [ { id: 'online-eval', title: 'Online Eval Config', description: 'Continuous evaluation pipeline' }, { id: 'gateway', title: 'Gateway', description: 'Route and manage gateway targets' }, { id: 'gateway-target', title: 'Gateway Target', description: 'Extend agent capabilities' }, - { id: 'runtime-endpoint', title: 'Runtime Endpoint', description: 'Named endpoint (version alias) for a runtime' }, + { id: 'runtime-endpoint', title: 'Runtime Endpoint', description: 'Named endpoint for a runtime' }, { id: 'policy', title: 'Policy', description: 'Cedar policies for gateway tools' }, ] as const; From c1ad7ea68da5569c66cfc3efc7bccbd2514d13d0 Mon Sep 17 00:00:00 2001 From: "T.J Ariyawansa" Date: Mon, 27 Apr 2026 15:34:00 -0400 Subject: [PATCH 05/18] fix: validate runtime endpoint version is a positive integer - Add explicit Number.isInteger check before schema validation - Change Commander parser from parseInt to Number so floats like 3.5 are caught instead of silently truncated --- src/cli/primitives/RuntimeEndpointPrimitive.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/cli/primitives/RuntimeEndpointPrimitive.ts b/src/cli/primitives/RuntimeEndpointPrimitive.ts index 8085dae1c..98732cba0 100644 --- a/src/cli/primitives/RuntimeEndpointPrimitive.ts +++ b/src/cli/primitives/RuntimeEndpointPrimitive.ts @@ -61,9 +61,15 @@ export class RuntimeEndpointPrimitive extends BasePrimitive', 'Runtime name to add the endpoint to') .requiredOption('--endpoint ', 'Endpoint name (e.g., prod, staging)') - .option('--version ', 'Version number to alias (default: 1)', parseInt) + .option('--version ', 'Version number to alias (default: 1)', Number) .option('--description ', 'Description of the endpoint') .option('--json', 'Output as JSON [non-interactive]') .action( From f3face838bc7d9802a2807811f3ec721675cfece Mon Sep 17 00:00:00 2001 From: "T.J Ariyawansa" Date: Mon, 27 Apr 2026 15:34:18 -0400 Subject: [PATCH 06/18] fix: use agent/endpoint composite key to prevent React key collision Endpoint names can collide across runtimes (e.g., both have "prod"). Changed React key from epName to agent.name/epName to prevent duplicate key warnings that pollute the TUI viewport. --- src/cli/tui/components/ResourceGraph.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli/tui/components/ResourceGraph.tsx b/src/cli/tui/components/ResourceGraph.tsx index 88309dd4f..44bcf0b77 100644 --- a/src/cli/tui/components/ResourceGraph.tsx +++ b/src/cli/tui/components/ResourceGraph.tsx @@ -199,7 +199,7 @@ export function ResourceGraph({ project, mcp, agentName, resourceStatuses }: Res const epState = parentState === 'deployed' ? 'deployed' : 'local-only'; const badge = getDeploymentBadge(epState); return ( - + {' '} {ICONS['runtime-endpoint']} {epName} v{ep.version} From 21147b1266fff0f73d3b6ddcaa4482e8554ef05c Mon Sep 17 00:00:00 2001 From: "T.J Ariyawansa" Date: Mon, 27 Apr 2026 15:34:38 -0400 Subject: [PATCH 07/18] fix: render runtime endpoints in status --type runtime-endpoint When filtering by --type runtime-endpoint, agents array is empty so the agents section (which nests endpoints) never renders. Added a standalone Runtime Endpoints section that shows when endpoints exist but agents don't (i.e., when type-filtering). --- src/cli/commands/status/command.tsx | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/cli/commands/status/command.tsx b/src/cli/commands/status/command.tsx index 0d018e58f..bb34a93bf 100644 --- a/src/cli/commands/status/command.tsx +++ b/src/cli/commands/status/command.tsx @@ -180,6 +180,20 @@ export const registerStatus = (program: Command) => { )} + {agents.length === 0 && runtimeEndpoints.length > 0 && ( + + Runtime Endpoints + {runtimeEndpoints.map(ep => ( + + {' '}◉ {ep.name} {ep.detail}{' '} + + [{DEPLOYMENT_STATE_LABELS[ep.deploymentState] ?? ep.deploymentState}] + + + ))} + + )} + {memories.length > 0 && ( Memories From 132f969c3b644b2457a8600cd9f3827de6c5d508 Mon Sep 17 00:00:00 2001 From: "T.J Ariyawansa" Date: Mon, 27 Apr 2026 15:34:50 -0400 Subject: [PATCH 08/18] fix: add runtime-endpoint to status --help --type documentation The --type option help text was missing runtime-endpoint from the list of valid resource types. --- src/cli/commands/status/command.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli/commands/status/command.tsx b/src/cli/commands/status/command.tsx index bb34a93bf..8ad84d0a1 100644 --- a/src/cli/commands/status/command.tsx +++ b/src/cli/commands/status/command.tsx @@ -59,7 +59,7 @@ export const registerStatus = (program: Command) => { .option('--target ', 'Select deployment target') .option( '--type ', - 'Filter by resource type (agent, memory, credential, gateway, evaluator, online-eval, policy-engine, policy)' + 'Filter by resource type (agent, runtime-endpoint, memory, credential, gateway, evaluator, online-eval, policy-engine, policy)' ) .option('--state ', 'Filter by deployment state (deployed, local-only, pending-removal)') .option('--runtime ', 'Filter to a specific runtime') From 3aba3e920467525412a3c5e1ce82b35158a478cf Mon Sep 17 00:00:00 2001 From: "T.J Ariyawansa" Date: Mon, 27 Apr 2026 15:35:02 -0400 Subject: [PATCH 09/18] fix: return richer JSON response from add runtime-endpoint add now returns { success, endpointName, agent, version } instead of sparse { success: true }, matching the richer response shape from remove runtime-endpoint. --- src/cli/primitives/RuntimeEndpointPrimitive.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/cli/primitives/RuntimeEndpointPrimitive.ts b/src/cli/primitives/RuntimeEndpointPrimitive.ts index 98732cba0..f7e1af384 100644 --- a/src/cli/primitives/RuntimeEndpointPrimitive.ts +++ b/src/cli/primitives/RuntimeEndpointPrimitive.ts @@ -80,7 +80,12 @@ export class RuntimeEndpointPrimitive extends BasePrimitive Date: Mon, 27 Apr 2026 15:38:51 -0400 Subject: [PATCH 10/18] fix: validate endpoint version against deployed runtime version - TUI: show "Current deployed version: N" and valid range (1-N) - TUI: reject version exceeding latest deployed version - CLI: check deployed-state.json for max version, reject if exceeded - If runtime not deployed, only positive integer check applies --- .../primitives/RuntimeEndpointPrimitive.ts | 21 ++++++++++- .../AddRuntimeEndpointFlow.tsx | 35 ++++++++++++++++--- .../AddRuntimeEndpointScreen.tsx | 27 ++++++++++++-- 3 files changed, 76 insertions(+), 7 deletions(-) diff --git a/src/cli/primitives/RuntimeEndpointPrimitive.ts b/src/cli/primitives/RuntimeEndpointPrimitive.ts index f7e1af384..056fa1da4 100644 --- a/src/cli/primitives/RuntimeEndpointPrimitive.ts +++ b/src/cli/primitives/RuntimeEndpointPrimitive.ts @@ -1,4 +1,4 @@ -import { findConfigRoot } from '../../lib'; +import { ConfigIO, findConfigRoot } from '../../lib'; import type { AgentCoreProjectSpec } from '../../schema'; import { RuntimeEndpointSchema } from '../../schema'; import type { ResourceType } from '../commands/remove/types'; @@ -67,6 +67,25 @@ export class RuntimeEndpointPrimitive extends BasePrimitive deployedRuntime.runtimeVersion) { + return { + success: false, + error: `Version ${version} exceeds latest deployed version ${deployedRuntime.runtimeVersion} for runtime "${options.runtime}".`, + }; + } + } + } + } catch { + // Deployed state may not exist or be readable — skip version range check + } + // Build and validate the endpoint config const config = { version, diff --git a/src/cli/tui/screens/runtime-endpoint/AddRuntimeEndpointFlow.tsx b/src/cli/tui/screens/runtime-endpoint/AddRuntimeEndpointFlow.tsx index 06838913e..2404109fc 100644 --- a/src/cli/tui/screens/runtime-endpoint/AddRuntimeEndpointFlow.tsx +++ b/src/cli/tui/screens/runtime-endpoint/AddRuntimeEndpointFlow.tsx @@ -6,9 +6,12 @@ import { AddRuntimeEndpointScreen } from './AddRuntimeEndpointScreen'; import type { RuntimeEndpointWizardConfig } from './types'; import React, { useCallback, useEffect, useState } from 'react'; +/** Map of runtime name → latest deployed version (undefined if not deployed) */ +export type RuntimeVersionMap = Record; + type FlowState = | { name: 'loading' } - | { name: 'create-wizard'; runtimeNames: string[] } + | { name: 'create-wizard'; runtimeNames: string[]; runtimeVersions: RuntimeVersionMap } | { name: 'create-success'; endpointName: string; runtimeName: string } | { name: 'error'; message: string }; @@ -29,7 +32,7 @@ export function AddRuntimeEndpointFlow({ }: AddRuntimeEndpointFlowProps) { const [flow, setFlow] = useState({ name: 'loading' }); - // Load runtimes on mount + // Load runtimes and deployed version info on mount useEffect(() => { void (async () => { try { @@ -40,7 +43,26 @@ export function AddRuntimeEndpointFlow({ setFlow({ name: 'error', message: 'No runtimes found. Add a runtime first with `agentcore add agent`.' }); return; } - setFlow({ name: 'create-wizard', runtimeNames }); + + // Load deployed state to get version info per runtime + const runtimeVersions: RuntimeVersionMap = {}; + if (configIO.configExists('state')) { + try { + const deployedState = await configIO.readDeployedState(); + for (const target of Object.values(deployedState.targets)) { + const runtimes = target.resources?.runtimes ?? {}; + for (const [name, state] of Object.entries(runtimes)) { + if (state.runtimeVersion) { + runtimeVersions[name] = state.runtimeVersion; + } + } + } + } catch { + // Deployed state may not exist yet — that's fine + } + } + + setFlow({ name: 'create-wizard', runtimeNames, runtimeVersions }); } catch (err) { const message = err instanceof Error ? err.message : String(err); setFlow({ name: 'error', message }); @@ -82,7 +104,12 @@ export function AddRuntimeEndpointFlow({ if (flow.name === 'create-wizard') { return ( - + ); } diff --git a/src/cli/tui/screens/runtime-endpoint/AddRuntimeEndpointScreen.tsx b/src/cli/tui/screens/runtime-endpoint/AddRuntimeEndpointScreen.tsx index 489b609a9..f2d3ee42c 100644 --- a/src/cli/tui/screens/runtime-endpoint/AddRuntimeEndpointScreen.tsx +++ b/src/cli/tui/screens/runtime-endpoint/AddRuntimeEndpointScreen.tsx @@ -2,6 +2,7 @@ import type { SelectableItem } from '../../components'; import { ConfirmReview, Cursor, Panel, Screen, StepIndicator, WizardSelect } from '../../components'; import { HELP_TEXT } from '../../constants'; import { useListNavigation } from '../../hooks'; +import type { RuntimeVersionMap } from './AddRuntimeEndpointFlow'; import type { RuntimeEndpointWizardConfig, RuntimeEndpointWizardStep } from './types'; import { useAddRuntimeEndpointWizard } from './useAddRuntimeEndpointWizard'; import { Box, Text, useInput } from 'ink'; @@ -20,9 +21,15 @@ interface AddRuntimeEndpointScreenProps { onComplete: (config: RuntimeEndpointWizardConfig) => void; onExit: () => void; runtimeNames: string[]; + runtimeVersions: RuntimeVersionMap; } -export function AddRuntimeEndpointScreen({ onComplete, onExit, runtimeNames }: AddRuntimeEndpointScreenProps) { +export function AddRuntimeEndpointScreen({ + onComplete, + onExit, + runtimeNames, + runtimeVersions, +}: AddRuntimeEndpointScreenProps) { const skipRuntimeStep = runtimeNames.length === 1; const wizard = useAddRuntimeEndpointWizard({ skipRuntimeStep }); @@ -48,6 +55,10 @@ export function AddRuntimeEndpointScreen({ onComplete, onExit, runtimeNames }: A const isEndpointStep = wizard.step === 'endpoint'; const isConfirmStep = wizard.step === 'confirm'; + // Get the max deployed version for the selected runtime + const maxVersion = effectiveConfig.runtimeName ? runtimeVersions[effectiveConfig.runtimeName] : undefined; + const isDeployed = maxVersion !== undefined; + // Multi-field state for endpoint step (CustomClaimForm pattern) const [activeField, setActiveField] = useState('name'); const [endpointName, setEndpointName] = useState(''); @@ -115,6 +126,10 @@ export function AddRuntimeEndpointScreen({ onComplete, onExit, runtimeNames }: A setError('Version must be a positive integer'); return; } + if (isDeployed && num > maxVersion) { + setError(`Version must be between 1 and ${maxVersion} (latest deployed version)`); + return; + } } setActiveField(ENDPOINT_FIELDS[idx + 1]!); setError(null); @@ -134,6 +149,10 @@ export function AddRuntimeEndpointScreen({ onComplete, onExit, runtimeNames }: A setError('Version must be a positive integer'); return; } + if (isDeployed && ver > maxVersion) { + setError(`Version must be between 1 and ${maxVersion} (latest deployed version)`); + return; + } const desc = endpointDescription.trim() || undefined; wizard.setEndpointDetails(endpointName.trim(), ver, desc); return; @@ -202,6 +221,9 @@ export function AddRuntimeEndpointScreen({ onComplete, onExit, runtimeNames }: A {isEndpointStep && ( Runtime: {effectiveConfig.runtimeName} + {isDeployed && ( + Current deployed version: {maxVersion} + )} Endpoint name: @@ -213,7 +235,8 @@ export function AddRuntimeEndpointScreen({ onComplete, onExit, runtimeNames }: A - Version: + + Version{isDeployed ? ` (1-${maxVersion})` : ''}: {activeField === 'version' && !endpointVersion && } {endpointVersion || 1} From 844689ac1c661b638549c014e69f18a948701df8 Mon Sep 17 00:00:00 2001 From: "T.J Ariyawansa" Date: Mon, 27 Apr 2026 16:50:18 -0400 Subject: [PATCH 11/18] chore: remove planning and bug bash docs from PR --- docs/bug-bash-runtime-endpoints.md | 138 -- .../2026-04-24-runtime-endpoint-support.md | 1593 ----------------- 2 files changed, 1731 deletions(-) delete mode 100644 docs/bug-bash-runtime-endpoints.md delete mode 100644 docs/superpowers/plans/2026-04-24-runtime-endpoint-support.md diff --git a/docs/bug-bash-runtime-endpoints.md b/docs/bug-bash-runtime-endpoints.md deleted file mode 100644 index 32ee01dd6..000000000 --- a/docs/bug-bash-runtime-endpoints.md +++ /dev/null @@ -1,138 +0,0 @@ -# Runtime Endpoint Support — Bug Bash - -## Feature Overview - -AgentCore runtimes support multiple endpoints, each associated with a different version. This feature adds the ability -for users to register, manage, and deploy named endpoints (version aliases) for their runtimes via the CLI. - -**What are runtime endpoints?** A runtime endpoint is a named alias that points to a specific version of a deployed -runtime. For example, a user might have a runtime `my-agent` at version 5, with: - -- `prod` endpoint → version 3 (stable release) -- `staging` endpoint → version 5 (latest) -- `canary` endpoint → version 4 (testing rollback) - -This lets teams manage traffic routing and versioning without redeploying the runtime itself. - -**What was added:** - -- `agentcore add runtime-endpoint` — CLI command + TUI wizard to add an endpoint to a runtime -- `agentcore remove runtime-endpoint` — CLI command + TUI flow to remove an endpoint -- `agentcore status` — endpoints appear nested under their parent agent with deployment state -- `agentcore deploy` — endpoints are deployed as `AWS::BedrockAgentCore::RuntimeEndpoint` CloudFormation resources -- Schema: `endpoints` dictionary on each runtime in `agentcore.json` -- Deployed state: `runtimeEndpoints` section in `deployed-state.json` with endpoint IDs and ARNs - ---- - -## Test Flows - -### Flow 1: Add Runtime Endpoint (CLI) - -**Goal:** Verify the CLI command correctly adds endpoints to agentcore.json. - -| Step | Command | Expected | -| ---- | ---------------------------------------------------------------------------------------------------------------- | ---------------------------------------- | -| 1a | `agentcore add runtime-endpoint --runtime --endpoint prod --version 3 --description "Production" --json` | Success JSON, endpoint in agentcore.json | -| 1b | `agentcore add runtime-endpoint --runtime --endpoint staging --json` | Success, version defaults to 1 | -| 1c | `agentcore add runtime-endpoint --runtime NonExistent --endpoint test --json` | Error: runtime not found | -| 1d | `agentcore add runtime-endpoint --runtime --endpoint prod --json` (duplicate) | Error: already exists | -| 1e | `agentcore add runtime-endpoint --runtime --endpoint "bad name!" --json` | Error: invalid name | -| 1f | `agentcore add runtime-endpoint --runtime --endpoint test --version 0 --json` | Error: version must be >= 1 | - -### Flow 2: Add Runtime Endpoint (TUI) - -**Goal:** Verify the interactive TUI wizard works correctly. - -| Step | Action | Expected | -| ---- | ------------------------------------------------------------- | --------------------------------------------------------------- | -| 2a | `agentcore` → Add → Runtime Endpoint (single runtime project) | Skips runtime selection, goes directly to endpoint form | -| 2b | `agentcore` → Add → Runtime Endpoint (multi-runtime project) | Shows runtime picker first | -| 2c | In endpoint form, tab between fields | All 3 fields visible, active field highlighted in cyan | -| 2d | Fill name + version + description → Enter | Advances to confirm screen | -| 2e | Confirm screen shows correct values | Runtime, endpoint, version, description displayed | -| 2f | Press Enter on confirm | Success screen: "Run `agentcore deploy` to create the endpoint" | -| 2g | Press Esc on endpoint form | Goes back (to runtime picker or exits) | - -### Flow 3: Remove Runtime Endpoint (CLI) - -**Goal:** Verify the CLI command correctly removes endpoints from agentcore.json. - -| Step | Command | Expected | -| ---- | ------------------------------------------------------------- | ------------------------------------------------- | -| 3a | `agentcore remove runtime-endpoint --name prod --json` | Success, endpoint removed from agentcore.json | -| 3b | `agentcore remove runtime-endpoint --name nonexistent --json` | Error: not found | -| 3c | Verify agentcore.json after removal | Endpoint gone, other endpoints untouched | -| 3d | Remove last endpoint on a runtime | `endpoints` key removed entirely (not empty `{}`) | - -### Flow 4: Remove Runtime Endpoint (TUI) - -**Goal:** Verify the interactive remove flow works. - -| Step | Action | Expected | -| ---- | --------------------------------------- | --------------------------------------------------- | -| 4a | `agentcore` → Remove → Runtime Endpoint | Shows list of endpoints with runtime name + version | -| 4b | Select an endpoint | Shows confirmation screen with details | -| 4c | Confirm removal | Success screen with deploy reminder | -| 4d | Esc on selection | Goes back to resource type picker | - -### Flow 5: Status Display - -**Goal:** Verify endpoints appear correctly in status output. - -| Step | Command | Expected | -| ---- | ------------------------------------------ | ------------------------------------------------- | -| 5a | `agentcore status` (before deploy) | Endpoints nested under agents with `[Local only]` | -| 5b | `agentcore status --json` | `runtime-endpoint` entries in resources array | -| 5c | `agentcore status --type runtime-endpoint` | Only endpoint entries shown | -| 5d | Deployed agent + local-only agent | Correct state per endpoint after deploy | - -### Flow 6: Deploy + Verify in AWS - -**Goal:** Verify endpoints are created in AWS and tracked in deployed state. - -| Step | Action | Expected | -| ---- | ------------------------------------- | ------------------------------------------------------------- | -| 6a | `agentcore deploy` | Deploys without errors, CFN creates RuntimeEndpoint resources | -| 6b | Check CloudFormation stack | `AWS::BedrockAgentCore::RuntimeEndpoint` resources present | -| 6c | Check deployed-state.json | `runtimeEndpoints` section with endpoint IDs and ARNs | -| 6d | `agentcore status` after deploy | Endpoints show `[Deployed]` | -| 6e | AWS API: `get-agent-runtime-endpoint` | Endpoint exists with correct version and name | -| 6f | Remove endpoint + redeploy | Endpoint torn down in AWS | - -### Flow 7: Edge Cases - -**Goal:** Verify correct behavior in unusual scenarios. - -| Step | Scenario | Expected | -| ---- | -------------------------------------------------- | -------------------------------------- | -| 7a | Same endpoint name on different runtimes | Both work (namespaced by runtime) | -| 7b | Multiple endpoints on one runtime | All display correctly in status | -| 7c | Remove a runtime that has endpoints | Runtime + all its endpoints removed | -| 7d | Empty endpoints dict in agentcore.json | No errors, no endpoints shown | -| 7e | Manually edit agentcore.json with invalid endpoint | `agentcore validate` catches the error | -| 7f | Container build agent with endpoints | Works same as CodeZip | - ---- - -## Bug Tracking - -| Alias | Bug Type | Description | Screenshots | Notes | -| ----- | -------- | ----------- | ----------- | ----- | -| | | | | | -| | | | | | -| | | | | | -| | | | | | -| | | | | | -| | | | | | -| | | | | | -| | | | | | -| | | | | | -| | | | | | -| | | | | | -| | | | | | -| | | | | | -| | | | | | -| | | | | | - -**Bug Types:** `functional` | `ux` | `validation` | `display` | `deploy` | `crash` | `regression` diff --git a/docs/superpowers/plans/2026-04-24-runtime-endpoint-support.md b/docs/superpowers/plans/2026-04-24-runtime-endpoint-support.md deleted file mode 100644 index 8f5a557f9..000000000 --- a/docs/superpowers/plans/2026-04-24-runtime-endpoint-support.md +++ /dev/null @@ -1,1593 +0,0 @@ -# Runtime Endpoint Support Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or -> superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Allow users to register, manage, and deploy named endpoints (version aliases) for AgentCore runtimes via -`agentcore add runtime-endpoint` / `agentcore remove runtime-endpoint`, with full TUI support and CDK deployment. - -**Architecture:** Runtime endpoints are modeled as a sub-resource of runtimes (similar to how gateway targets are -sub-resources of gateways). An `endpoints` dictionary is added to `AgentEnvSpec` in the schema. The CLI primitive -follows the GatewayTargetPrimitive pattern — a sub-resource that validates its parent exists. The CDK construct creates -`CfnRuntimeEndpoint` resources for each endpoint entry. The `status` command and TUI display endpoints nested under -their parent runtime. - -**Tech Stack:** TypeScript, Zod (schema validation), Commander.js (CLI), Ink/React (TUI), AWS CDK (infrastructure) - ---- - -## File Structure - -### New Files (CLI repo: `agentcore-cli`) - -| File | Responsibility | -| --------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | -| `src/cli/primitives/RuntimeEndpointPrimitive.ts` | Primitive class: add/remove/preview/getRemovable/registerCommands for runtime endpoints | -| `src/cli/tui/screens/runtime-endpoint/AddRuntimeEndpointFlow.tsx` | TUI add flow wrapper (wizard state machine) | -| `src/cli/tui/screens/runtime-endpoint/AddRuntimeEndpointScreen.tsx` | TUI add screen (step-by-step input collection) | -| `src/cli/tui/screens/runtime-endpoint/useAddRuntimeEndpointWizard.ts` | React hook managing wizard steps and config state | -| `src/cli/tui/screens/runtime-endpoint/types.ts` | Types for the runtime endpoint wizard config | -| `src/cli/tui/screens/runtime-endpoint/index.ts` | Barrel exports | -| `src/cli/tui/screens/remove/RemoveRuntimeEndpointScreen.tsx` | TUI remove screen (list endpoints for selection) | - -### Modified Files (CLI repo: `agentcore-cli`) - -| File | Change | -| --------------------------------------------- | -------------------------------------------------------------------------------------------------------- | -| `src/schema/schemas/agent-env.ts` | Add `RuntimeEndpointSchema`, `endpoints` field to `AgentEnvSpecSchema` | -| `src/schema/schemas/deployed-state.ts` | Add `runtimeVersion` to `AgentCoreDeployedStateSchema` | -| `src/cli/primitives/registry.ts` | Add `runtimeEndpointPrimitive` singleton + to `ALL_PRIMITIVES` | -| `src/cli/primitives/index.ts` | Export `RuntimeEndpointPrimitive` | -| `src/cli/commands/remove/types.ts` | Add `'runtime-endpoint'` to `ResourceType` union | -| `src/cli/commands/status/action.ts` | Add `'runtime-endpoint'` to `ResourceStatusEntry.resourceType`, store `runtimeVersion` in deployed state | -| `src/cli/tui/screens/add/AddScreen.tsx` | Add `'runtime-endpoint'` to `ADD_RESOURCES` | -| `src/cli/tui/screens/add/AddFlow.tsx` | Add `'runtime-endpoint-wizard'` flow state + render `AddRuntimeEndpointFlow` | -| `src/cli/tui/screens/remove/RemoveFlow.tsx` | Add runtime-endpoint flow states, hooks, handlers, screens | -| `src/cli/tui/screens/remove/RemoveScreen.tsx` | Add `'runtime-endpoint'` to remove resource list | -| `src/cli/tui/hooks/useRemove.ts` | Add `useRemovableRuntimeEndpoints` and `useRemoveRuntimeEndpoint` hooks | -| `src/cli/tui/components/ResourceGraph.tsx` | Display endpoints nested under agents in status view | - -### Modified Files (CDK repo: `agentcore-l3-cdk-constructs`) - -| File | Change | -| ------------------------------------------------ | ----------------------------------------------------------------------------------- | -| `src/schema/schemas/agent-env.ts` | Add `RuntimeEndpointSchema`, `endpoints` field to `AgentEnvSpecSchema` (mirror CLI) | -| `src/cdk/constructs/l3/AgentEnvironment.ts` | Create `CfnRuntimeEndpoint` resources for each endpoint entry | -| `src/cdk/constructs/components/mcp/mcp-utils.ts` | Add `getRuntimeEndpointUrlWithQualifier()` function | - ---- - -## Task 1: Add `endpoints` field to AgentEnvSpec schema (CLI repo) - -**Files:** - -- Modify: `agentcore-cli/src/schema/schemas/agent-env.ts:195-272` -- Test: `agentcore-cli/src/schema/schemas/__tests__/agent-env.test.ts` - -- [ ] **Step 1: Write the failing test** - -In the existing agent-env test file, add tests for the new endpoints field: - -```typescript -describe('endpoints', () => { - it('accepts a valid endpoints dictionary', () => { - const spec = { - ...validCodeZipAgent, - endpoints: { - prod: { version: 3, description: 'Production traffic' }, - staging: { version: 2 }, - }, - }; - const result = AgentEnvSpecSchema.safeParse(spec); - expect(result.success).toBe(true); - if (result.success) { - expect(result.data.endpoints).toEqual({ - prod: { version: 3, description: 'Production traffic' }, - staging: { version: 2 }, - }); - } - }); - - it('defaults to undefined when endpoints is not provided', () => { - const result = AgentEnvSpecSchema.safeParse(validCodeZipAgent); - expect(result.success).toBe(true); - if (result.success) { - expect(result.data.endpoints).toBeUndefined(); - } - }); - - it('rejects endpoint name with invalid characters', () => { - const spec = { - ...validCodeZipAgent, - endpoints: { - 'invalid name!': { version: 1 }, - }, - }; - const result = AgentEnvSpecSchema.safeParse(spec); - expect(result.success).toBe(false); - }); - - it('rejects endpoint with version less than 1', () => { - const spec = { - ...validCodeZipAgent, - endpoints: { - prod: { version: 0 }, - }, - }; - const result = AgentEnvSpecSchema.safeParse(spec); - expect(result.success).toBe(false); - }); - - it('accepts endpoint with only version (description optional)', () => { - const spec = { - ...validCodeZipAgent, - endpoints: { - canary: { version: 1 }, - }, - }; - const result = AgentEnvSpecSchema.safeParse(spec); - expect(result.success).toBe(true); - }); -}); -``` - -- [ ] **Step 2: Run test to verify it fails** - -Run: -`cd /Users/tjariy/github_workspace/runtime_endpoint_project/agentcore-cli && npx vitest run src/schema/schemas/__tests__/agent-env.test.ts --reporter=verbose` -Expected: FAIL — `endpoints` field not recognized (strict schema rejects unknown keys) - -- [ ] **Step 3: Add RuntimeEndpointSchema and endpoints field to AgentEnvSpecSchema** - -In `agentcore-cli/src/schema/schemas/agent-env.ts`, add before the `AgentEnvSpecSchema` definition: - -```typescript -// ============================================================================ -// Runtime Endpoint Schema -// ============================================================================ - -/** - * Endpoint name follows the AgentCore API regex for endpoint aliases. - * https://docs.aws.amazon.com/bedrock-agentcore-control/latest/APIReference/API_CreateAgentRuntimeEndpoint.html - */ -export const RuntimeEndpointNameSchema = z - .string() - .min(1, 'Endpoint name is required') - .max(48) - .regex( - /^[a-zA-Z][a-zA-Z0-9_]{0,47}$/, - 'Must begin with a letter and contain only alphanumeric characters and underscores (max 48 chars)' - ); - -export const RuntimeEndpointSchema = z.object({ - /** Version number this endpoint points to. Must be >= 1. */ - version: z.number().int().min(1), - /** Optional human-readable description of this endpoint. */ - description: z.string().max(200).optional(), -}); - -export type RuntimeEndpoint = z.infer; -``` - -Then add the `endpoints` field to the `AgentEnvSpecSchema` object, after `filesystemConfigurations`: - -```typescript - /** Named endpoints (version aliases) for this runtime. Keys are endpoint names. */ - endpoints: z.record(RuntimeEndpointNameSchema, RuntimeEndpointSchema).optional(), -``` - -Export the new types from the file: - -```typescript -export { RuntimeEndpointNameSchema, RuntimeEndpointSchema }; -export type { RuntimeEndpoint }; -``` - -- [ ] **Step 4: Run test to verify it passes** - -Run: -`cd /Users/tjariy/github_workspace/runtime_endpoint_project/agentcore-cli && npx vitest run src/schema/schemas/__tests__/agent-env.test.ts --reporter=verbose` -Expected: PASS - -- [ ] **Step 5: Commit** - -```bash -cd /Users/tjariy/github_workspace/runtime_endpoint_project/agentcore-cli -git add src/schema/schemas/agent-env.ts src/schema/schemas/__tests__/agent-env.test.ts -git commit -m "feat: add endpoints field to AgentEnvSpec schema" -``` - ---- - -## Task 2: Add `runtimeVersion` to deployed state schema (CLI repo) - -**Files:** - -- Modify: `agentcore-cli/src/schema/schemas/deployed-state.ts:9-17` -- Test: `agentcore-cli/src/schema/schemas/__tests__/deployed-state.test.ts` - -- [ ] **Step 1: Write the failing test** - -```typescript -describe('AgentCoreDeployedStateSchema', () => { - it('accepts runtimeVersion field', () => { - const state = { - runtimeId: 'rt-123', - runtimeArn: 'arn:aws:bedrock-agentcore:us-east-1:123456789:runtime/rt-123', - roleArn: 'arn:aws:iam::123456789:role/test', - runtimeVersion: 3, - }; - const result = AgentCoreDeployedStateSchema.safeParse(state); - expect(result.success).toBe(true); - if (result.success) { - expect(result.data.runtimeVersion).toBe(3); - } - }); - - it('allows runtimeVersion to be omitted (backwards compatible)', () => { - const state = { - runtimeId: 'rt-123', - runtimeArn: 'arn:aws:bedrock-agentcore:us-east-1:123456789:runtime/rt-123', - roleArn: 'arn:aws:iam::123456789:role/test', - }; - const result = AgentCoreDeployedStateSchema.safeParse(state); - expect(result.success).toBe(true); - }); -}); -``` - -- [ ] **Step 2: Run test to verify it fails** - -Run: -`cd /Users/tjariy/github_workspace/runtime_endpoint_project/agentcore-cli && npx vitest run src/schema/schemas/__tests__/deployed-state.test.ts --reporter=verbose` -Expected: FAIL — `runtimeVersion` not in schema - -- [ ] **Step 3: Add runtimeVersion to AgentCoreDeployedStateSchema** - -In `agentcore-cli/src/schema/schemas/deployed-state.ts`, modify `AgentCoreDeployedStateSchema`: - -```typescript -export const AgentCoreDeployedStateSchema = z.object({ - runtimeId: z.string().min(1), - runtimeArn: z.string().min(1), - roleArn: z.string().min(1), - sessionId: z.string().optional(), - memoryIds: z.array(z.string()).optional(), - browserId: z.string().optional(), - codeInterpreterId: z.string().optional(), - /** The latest deployed version number of this runtime. */ - runtimeVersion: z.number().int().min(1).optional(), -}); -``` - -- [ ] **Step 4: Run test to verify it passes** - -Run: -`cd /Users/tjariy/github_workspace/runtime_endpoint_project/agentcore-cli && npx vitest run src/schema/schemas/__tests__/deployed-state.test.ts --reporter=verbose` -Expected: PASS - -- [ ] **Step 5: Commit** - -```bash -cd /Users/tjariy/github_workspace/runtime_endpoint_project/agentcore-cli -git add src/schema/schemas/deployed-state.ts src/schema/schemas/__tests__/deployed-state.test.ts -git commit -m "feat: add runtimeVersion to deployed state for version tracking" -``` - ---- - -## Task 3: Add `'runtime-endpoint'` to ResourceType union (CLI repo) - -**Files:** - -- Modify: `agentcore-cli/src/cli/commands/remove/types.ts:1-10` -- Modify: `agentcore-cli/src/cli/commands/status/action.ts:12-28` - -- [ ] **Step 1: Add 'runtime-endpoint' to ResourceType** - -In `agentcore-cli/src/cli/commands/remove/types.ts`: - -```typescript -export type ResourceType = - | 'agent' - | 'gateway' - | 'gateway-target' - | 'runtime-endpoint' - | 'memory' - | 'credential' - | 'evaluator' - | 'online-eval' - | 'policy-engine' - | 'policy'; -``` - -- [ ] **Step 2: Add 'runtime-endpoint' to ResourceStatusEntry.resourceType** - -In `agentcore-cli/src/cli/commands/status/action.ts`, update the `ResourceStatusEntry` interface: - -```typescript -export interface ResourceStatusEntry { - resourceType: - | 'agent' - | 'memory' - | 'credential' - | 'gateway' - | 'evaluator' - | 'online-eval' - | 'policy-engine' - | 'policy' - | 'runtime-endpoint'; - name: string; - deploymentState: ResourceDeploymentState; - identifier?: string; - detail?: string; - error?: string; - invocationUrl?: string; -} -``` - -- [ ] **Step 3: Commit** - -```bash -cd /Users/tjariy/github_workspace/runtime_endpoint_project/agentcore-cli -git add src/cli/commands/remove/types.ts src/cli/commands/status/action.ts -git commit -m "feat: add runtime-endpoint to ResourceType and ResourceStatusEntry" -``` - ---- - -## Task 4: Create RuntimeEndpointPrimitive (CLI repo) - -**Files:** - -- Create: `agentcore-cli/src/cli/primitives/RuntimeEndpointPrimitive.ts` -- Test: `agentcore-cli/src/cli/primitives/__tests__/RuntimeEndpointPrimitive.test.ts` - -- [ ] **Step 1: Write the failing test** - -```typescript -import type { AgentCoreProjectSpec } from '../../../schema'; -import { RuntimeEndpointPrimitive } from '../RuntimeEndpointPrimitive'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; - -// We'll test the primitive's core logic via its public methods -// Mock ConfigIO to avoid filesystem access -vi.mock('../../../lib', () => ({ - ConfigIO: vi.fn().mockImplementation(() => ({ - readProjectSpec: vi.fn(), - writeProjectSpec: vi.fn(), - configExists: vi.fn().mockReturnValue(false), - })), - findConfigRoot: vi.fn().mockReturnValue('/mock/agentcore'), - requireConfigRoot: vi.fn().mockReturnValue('/mock/agentcore'), -})); - -describe('RuntimeEndpointPrimitive', () => { - let primitive: RuntimeEndpointPrimitive; - - beforeEach(() => { - primitive = new RuntimeEndpointPrimitive(); - }); - - it('has kind "runtime-endpoint"', () => { - expect(primitive.kind).toBe('runtime-endpoint'); - }); - - it('has label "Runtime Endpoint"', () => { - expect(primitive.label).toBe('Runtime Endpoint'); - }); -}); -``` - -- [ ] **Step 2: Run test to verify it fails** - -Run: -`cd /Users/tjariy/github_workspace/runtime_endpoint_project/agentcore-cli && npx vitest run src/cli/primitives/__tests__/RuntimeEndpointPrimitive.test.ts --reporter=verbose` -Expected: FAIL — module not found - -- [ ] **Step 3: Create RuntimeEndpointPrimitive** - -Create `agentcore-cli/src/cli/primitives/RuntimeEndpointPrimitive.ts`: - -```typescript -import { findConfigRoot } from '../../lib'; -import type { AgentCoreProjectSpec, RuntimeEndpoint } from '../../schema'; -import { RuntimeEndpointSchema } from '../../schema'; -import type { ResourceType } from '../commands/remove/types'; -import { getErrorMessage } from '../errors'; -import type { SchemaChange } from '../operations/remove/types'; -import { BasePrimitive } from './BasePrimitive'; -import { SOURCE_CODE_NOTE } from './constants'; -import type { AddResult, AddScreenComponent, RemovableResource, RemovalPreview, RemovalResult } from './types'; -import type { Command } from '@commander-js/extra-typings'; - -/** - * Options for adding a runtime endpoint (CLI-level). - */ -export interface AddRuntimeEndpointOptions { - runtime: string; - endpoint: string; - version?: number; - description?: string; -} - -/** - * Removable runtime endpoint with parent runtime info. - */ -export interface RemovableRuntimeEndpoint extends RemovableResource { - runtimeName: string; - endpointName: string; - version: number; - description?: string; -} - -/** - * RuntimeEndpointPrimitive handles all runtime endpoint add/remove operations. - * Follows the sub-resource pattern established by GatewayTargetPrimitive. - */ -export class RuntimeEndpointPrimitive extends BasePrimitive { - readonly kind: ResourceType = 'runtime-endpoint'; - readonly label = 'Runtime Endpoint'; - readonly primitiveSchema = RuntimeEndpointSchema; - - async add(options: AddRuntimeEndpointOptions): Promise { - try { - const project = await this.readProjectSpec(); - - // Validate runtime exists - const runtime = project.runtimes.find(r => r.name === options.runtime); - if (!runtime) { - return { success: false, error: `Runtime "${options.runtime}" not found in agentcore.json.` }; - } - - // Initialize endpoints if needed - if (!runtime.endpoints) { - runtime.endpoints = {}; - } - - // Check for duplicate endpoint name - if (runtime.endpoints[options.endpoint]) { - return { - success: false, - error: `Endpoint "${options.endpoint}" already exists on runtime "${options.runtime}".`, - }; - } - - // Build endpoint config - const endpointConfig: RuntimeEndpoint = { - version: options.version ?? 1, - ...(options.description && { description: options.description }), - }; - - // Validate - RuntimeEndpointSchema.parse(endpointConfig); - - runtime.endpoints[options.endpoint] = endpointConfig; - await this.writeProjectSpec(project); - - return { success: true }; - } catch (err) { - return { success: false, error: getErrorMessage(err) }; - } - } - - async remove(name: string): Promise { - try { - const project = await this.readProjectSpec(); - - // Find the endpoint across all runtimes - for (const runtime of project.runtimes) { - if (runtime.endpoints && runtime.endpoints[name]) { - delete runtime.endpoints[name]; - // Clean up empty endpoints object - if (Object.keys(runtime.endpoints).length === 0) { - delete runtime.endpoints; - } - await this.writeProjectSpec(project); - return { success: true }; - } - } - - return { success: false, error: `Runtime endpoint "${name}" not found.` }; - } catch (err) { - return { success: false, error: getErrorMessage(err) }; - } - } - - async previewRemove(name: string): Promise { - const project = await this.readProjectSpec(); - - for (const runtime of project.runtimes) { - if (runtime.endpoints && runtime.endpoints[name]) { - const endpoint = runtime.endpoints[name]; - const summary = [ - `Removing endpoint "${name}" from runtime "${runtime.name}"`, - `Version: ${endpoint.version}`, - ...(endpoint.description ? [`Description: ${endpoint.description}`] : []), - ]; - - // Check cross-references: gateway targets that use this endpoint as a qualifier - const blockingRefs = this.findGatewayTargetReferences(project, runtime.name, name); - if (blockingRefs.length > 0) { - const refList = blockingRefs.map(ref => `"${ref.targetName}" in gateway "${ref.gatewayName}"`).join(', '); - throw new Error( - `Cannot remove endpoint "${name}" — it is referenced by gateway target ${refList}. Remove the gateway target first.` - ); - } - - const afterProject = structuredClone(project); - const afterRuntime = afterProject.runtimes.find(r => r.name === runtime.name)!; - delete afterRuntime.endpoints![name]; - if (Object.keys(afterRuntime.endpoints!).length === 0) { - delete afterRuntime.endpoints; - } - - const schemaChanges: SchemaChange[] = [ - { - file: 'agentcore/agentcore.json', - before: project, - after: afterProject, - }, - ]; - - return { summary, directoriesToDelete: [], schemaChanges }; - } - } - - throw new Error(`Runtime endpoint "${name}" not found.`); - } - - async getRemovable(): Promise { - try { - const project = await this.readProjectSpec(); - const endpoints: RemovableRuntimeEndpoint[] = []; - - for (const runtime of project.runtimes) { - if (runtime.endpoints) { - for (const [endpointName, config] of Object.entries(runtime.endpoints)) { - endpoints.push({ - name: endpointName, - type: 'runtime-endpoint', - runtimeName: runtime.name, - endpointName, - version: config.version, - description: config.description, - }); - } - } - } - - return endpoints; - } catch { - return []; - } - } - - registerCommands(addCmd: Command, removeCmd: Command): void { - addCmd - .command('runtime-endpoint') - .description('Add a named endpoint (version alias) to a runtime') - .requiredOption('--runtime ', 'Runtime to add the endpoint to') - .requiredOption('--endpoint ', 'Endpoint name') - .option('--version ', 'Version number (defaults to latest)', parseInt) - .option('--description ', 'Endpoint description') - .option('--json', 'Output as JSON [non-interactive]') - .action( - async (cliOptions: { - runtime: string; - endpoint: string; - version?: number; - description?: string; - json?: boolean; - }) => { - try { - if (!findConfigRoot()) { - console.error('No agentcore project found. Run `agentcore create` first.'); - process.exit(1); - } - - const result = await this.add({ - runtime: cliOptions.runtime, - endpoint: cliOptions.endpoint, - version: cliOptions.version, - description: cliOptions.description, - }); - - if (cliOptions.json) { - console.log(JSON.stringify(result)); - } else if (result.success) { - console.log(`Added endpoint '${cliOptions.endpoint}' to runtime '${cliOptions.runtime}'.`); - console.log('Run `agentcore deploy` to create the endpoint.'); - } else { - console.error(result.error); - } - - process.exit(result.success ? 0 : 1); - } catch (error) { - if (cliOptions.json) { - console.log(JSON.stringify({ success: false, error: getErrorMessage(error) })); - } else { - console.error(`Error: ${getErrorMessage(error)}`); - } - process.exit(1); - } - } - ); - - removeCmd - .command('runtime-endpoint') - .description('Remove a runtime endpoint from the project') - .option('--name ', 'Name of endpoint to remove [non-interactive]') - .option('-y, --yes', 'Skip confirmation prompt [non-interactive]') - .option('--json', 'Output as JSON [non-interactive]') - .action(async (cliOptions: { name?: string; yes?: boolean; json?: boolean }) => { - try { - if (!findConfigRoot()) { - console.error('No agentcore project found. Run `agentcore create` first.'); - process.exit(1); - } - - if (cliOptions.name || cliOptions.yes || cliOptions.json) { - if (!cliOptions.name) { - console.log(JSON.stringify({ success: false, error: '--name is required' })); - process.exit(1); - } - - const result = await this.remove(cliOptions.name); - console.log( - JSON.stringify({ - success: result.success, - resourceType: this.kind, - resourceName: cliOptions.name, - message: result.success ? `Removed runtime endpoint '${cliOptions.name}'` : undefined, - note: result.success ? SOURCE_CODE_NOTE : undefined, - error: !result.success ? result.error : undefined, - }) - ); - process.exit(result.success ? 0 : 1); - } else { - // TUI fallback - const [{ render }, { default: React }, { RemoveFlow }] = await Promise.all([ - import('ink'), - import('react'), - import('../tui/screens/remove'), - ]); - const { clear, unmount } = render( - React.createElement(RemoveFlow, { - isInteractive: false, - force: cliOptions.yes, - initialResourceType: this.kind, - initialResourceName: cliOptions.name, - onExit: () => { - clear(); - unmount(); - process.exit(0); - }, - }) - ); - } - } catch (error) { - if (cliOptions.json) { - console.log(JSON.stringify({ success: false, error: getErrorMessage(error) })); - } else { - console.error(`Error: ${getErrorMessage(error)}`); - } - process.exit(1); - } - }); - } - - addScreen(): AddScreenComponent { - return null; // TUI add screen is handled via AddRuntimeEndpointFlow in AddFlow.tsx - } - - /** - * Find gateway targets that reference a specific runtime endpoint as a qualifier. - * Used for cross-reference validation during remove. - */ - private findGatewayTargetReferences( - project: AgentCoreProjectSpec, - _runtimeName: string, - _endpointName: string - ): { gatewayName: string; targetName: string }[] { - // Future: when gateway targets support endpoint qualifiers, check here. - // For now, return empty — no cross-references exist yet. - return []; - } -} -``` - -- [ ] **Step 4: Run test to verify it passes** - -Run: -`cd /Users/tjariy/github_workspace/runtime_endpoint_project/agentcore-cli && npx vitest run src/cli/primitives/__tests__/RuntimeEndpointPrimitive.test.ts --reporter=verbose` -Expected: PASS - -- [ ] **Step 5: Commit** - -```bash -cd /Users/tjariy/github_workspace/runtime_endpoint_project/agentcore-cli -git add src/cli/primitives/RuntimeEndpointPrimitive.ts src/cli/primitives/__tests__/RuntimeEndpointPrimitive.test.ts -git commit -m "feat: add RuntimeEndpointPrimitive with add/remove/preview logic" -``` - ---- - -## Task 5: Register primitive in registry and exports (CLI repo) - -**Files:** - -- Modify: `agentcore-cli/src/cli/primitives/registry.ts` -- Modify: `agentcore-cli/src/cli/primitives/index.ts` - -- [ ] **Step 1: Add to registry.ts** - -Add import at top: - -```typescript -import { RuntimeEndpointPrimitive } from './RuntimeEndpointPrimitive'; -``` - -Add singleton after `policyPrimitive`: - -```typescript -export const runtimeEndpointPrimitive = new RuntimeEndpointPrimitive(); -``` - -Add to `ALL_PRIMITIVES` array (after `policyPrimitive`): - -```typescript -export const ALL_PRIMITIVES: BasePrimitive[] = [ - agentPrimitive, - memoryPrimitive, - credentialPrimitive, - evaluatorPrimitive, - onlineEvalConfigPrimitive, - gatewayPrimitive, - gatewayTargetPrimitive, - policyEnginePrimitive, - policyPrimitive, - runtimeEndpointPrimitive, -]; -``` - -- [ ] **Step 2: Add to index.ts barrel export** - -In `agentcore-cli/src/cli/primitives/index.ts`, add: - -```typescript -export { RuntimeEndpointPrimitive } from './RuntimeEndpointPrimitive'; -export type { AddRuntimeEndpointOptions, RemovableRuntimeEndpoint } from './RuntimeEndpointPrimitive'; -``` - -- [ ] **Step 3: Verify build passes** - -Run: `cd /Users/tjariy/github_workspace/runtime_endpoint_project/agentcore-cli && npx tsc --noEmit` Expected: No errors - -- [ ] **Step 4: Commit** - -```bash -cd /Users/tjariy/github_workspace/runtime_endpoint_project/agentcore-cli -git add src/cli/primitives/registry.ts src/cli/primitives/index.ts -git commit -m "feat: register RuntimeEndpointPrimitive in registry" -``` - ---- - -## Task 6: Add runtime-endpoint to TUI Add flow (CLI repo) - -**Files:** - -- Modify: `agentcore-cli/src/cli/tui/screens/add/AddScreen.tsx:4-13` -- Modify: `agentcore-cli/src/cli/tui/screens/add/AddFlow.tsx` -- Create: `agentcore-cli/src/cli/tui/screens/runtime-endpoint/types.ts` -- Create: `agentcore-cli/src/cli/tui/screens/runtime-endpoint/useAddRuntimeEndpointWizard.ts` -- Create: `agentcore-cli/src/cli/tui/screens/runtime-endpoint/AddRuntimeEndpointScreen.tsx` -- Create: `agentcore-cli/src/cli/tui/screens/runtime-endpoint/AddRuntimeEndpointFlow.tsx` -- Create: `agentcore-cli/src/cli/tui/screens/runtime-endpoint/index.ts` - -- [ ] **Step 1: Create types.ts** - -Create `agentcore-cli/src/cli/tui/screens/runtime-endpoint/types.ts`: - -```typescript -export interface RuntimeEndpointWizardConfig { - runtimeName: string; - endpointName: string; - version: number; - description?: string; -} - -export type RuntimeEndpointWizardStep = 'runtime' | 'endpoint' | 'confirm'; -``` - -- [ ] **Step 2: Create useAddRuntimeEndpointWizard.ts** - -Create `agentcore-cli/src/cli/tui/screens/runtime-endpoint/useAddRuntimeEndpointWizard.ts`: - -```typescript -import type { RuntimeEndpointWizardConfig, RuntimeEndpointWizardStep } from './types'; -import { useCallback, useMemo, useState } from 'react'; - -const BASE_STEPS: RuntimeEndpointWizardStep[] = ['runtime', 'endpoint', 'confirm']; - -export function useAddRuntimeEndpointWizard(runtimes: { name: string }[]) { - const [step, setStep] = useState(runtimes.length === 1 ? 'endpoint' : 'runtime'); - const [config, setConfig] = useState>(() => ({ - runtimeName: runtimes.length === 1 ? runtimes[0].name : undefined, - version: 1, - })); - - const steps = useMemo(() => { - if (runtimes.length === 1) { - return BASE_STEPS.filter(s => s !== 'runtime'); - } - return BASE_STEPS; - }, [runtimes.length]); - - const nextStep = useCallback( - (current: RuntimeEndpointWizardStep) => { - const idx = steps.indexOf(current); - return idx < steps.length - 1 ? steps[idx + 1] : undefined; - }, - [steps] - ); - - const goBack = useCallback(() => { - const idx = steps.indexOf(step); - if (idx > 0) { - setStep(steps[idx - 1]); - } - }, [step, steps]); - - const setRuntime = useCallback( - (name: string) => { - setConfig(c => ({ ...c, runtimeName: name })); - const next = nextStep('runtime'); - if (next) setStep(next); - }, - [nextStep] - ); - - const setEndpointDetails = useCallback( - (endpointName: string, version: number, description?: string) => { - setConfig(c => ({ ...c, endpointName, version, description })); - const next = nextStep('endpoint'); - if (next) setStep(next); - }, - [nextStep] - ); - - return { - step, - config: config as RuntimeEndpointWizardConfig, - steps, - goBack, - setRuntime, - setEndpointDetails, - }; -} -``` - -- [ ] **Step 3: Create AddRuntimeEndpointScreen.tsx** - -Create `agentcore-cli/src/cli/tui/screens/runtime-endpoint/AddRuntimeEndpointScreen.tsx`: - -```typescript -import type { AgentEnvSpec } from '../../../../schema'; -import { Panel, Screen, TextInput, WizardSelect } from '../../components'; -import { useAddRuntimeEndpointWizard } from './useAddRuntimeEndpointWizard'; -import type { RuntimeEndpointWizardConfig } from './types'; -import { Box, Text, useInput } from 'ink'; -import React, { useState } from 'react'; - -interface AddRuntimeEndpointScreenProps { - runtimes: AgentEnvSpec[]; - onComplete: (config: RuntimeEndpointWizardConfig) => void; - onBack: () => void; - onExit: () => void; -} - -export function AddRuntimeEndpointScreen({ - runtimes, - onComplete, - onBack, - onExit, -}: AddRuntimeEndpointScreenProps) { - const wizard = useAddRuntimeEndpointWizard(runtimes); - const [endpointName, setEndpointName] = useState(''); - const [version, setVersion] = useState('1'); - const [description, setDescription] = useState(''); - const [inputField, setInputField] = useState<'name' | 'version' | 'description'>('name'); - - const stepLabels = wizard.steps.map((s, i) => { - const idx = wizard.steps.indexOf(wizard.step); - const label = s === 'runtime' ? 'Runtime' : s === 'endpoint' ? 'Endpoint' : 'Confirm'; - if (i < idx) return `✓ ${label}`; - if (i === idx) return `[${i + 1} ${label}]`; - return `${i + 1} ${label}`; - }); - - const handleBack = () => { - if (wizard.steps.indexOf(wizard.step) === 0) { - onBack(); - } else { - wizard.goBack(); - } - }; - - useInput((_input, key) => { - if (key.escape) { - handleBack(); - } - }); - - return ( - - - Step: {stepLabels.join(' → ')} - - {wizard.step === 'runtime' && ( - ({ - id: r.name, - title: r.name, - description: r.build, - }))} - onSelect={item => wizard.setRuntime(item.id)} - isActive={true} - /> - )} - - {wizard.step === 'endpoint' && ( - - Runtime: {wizard.config.runtimeName} - - {inputField === 'name' && ( - - Endpoint name: - { - if (endpointName.trim()) setInputField('version'); - }} - /> - - )} - {inputField === 'version' && ( - - Endpoint name: {endpointName} - - Version: - setInputField('description')} - /> - - - )} - {inputField === 'description' && ( - - Endpoint name: {endpointName} - Version: {version} - - Description (optional): - { - wizard.setEndpointDetails( - endpointName.trim(), - parseInt(version, 10) || 1, - description.trim() || undefined - ); - }} - /> - - - )} - - - )} - - {wizard.step === 'confirm' && ( - - Review Runtime Endpoint - {'─'.repeat(25)} - Runtime: {wizard.config.runtimeName} - Endpoint: {wizard.config.endpointName} - Version: {wizard.config.version} - {wizard.config.description && ( - Description: {wizard.config.description} - )} - - ⏎ confirm esc go back - - - )} - - - - ); -} -``` - -Note: The confirm step's enter-to-submit behavior needs to be wired via `useInput` — the implementer should check the -existing patterns in `AddMemoryScreen.tsx` for the exact `useInput` + confirm pattern and replicate it. - -- [ ] **Step 4: Create AddRuntimeEndpointFlow.tsx** - -Create `agentcore-cli/src/cli/tui/screens/runtime-endpoint/AddRuntimeEndpointFlow.tsx`: - -```typescript -import { ConfigIO } from '../../../../lib'; -import type { AgentEnvSpec } from '../../../../schema'; -import { runtimeEndpointPrimitive } from '../../../primitives/registry'; -import { ErrorPrompt } from '../../components'; -import { AddSuccessScreen } from '../add/AddSuccessScreen'; -import { AddRuntimeEndpointScreen } from './AddRuntimeEndpointScreen'; -import type { RuntimeEndpointWizardConfig } from './types'; -import React, { useCallback, useEffect, useState } from 'react'; - -type FlowState = - | { name: 'loading' } - | { name: 'create-wizard'; runtimes: AgentEnvSpec[] } - | { name: 'create-success'; endpointName: string; runtimeName: string } - | { name: 'error'; message: string }; - -interface AddRuntimeEndpointFlowProps { - isInteractive: boolean; - onBack: () => void; - onExit: () => void; - onDev?: () => void; - onDeploy?: () => void; -} - -export function AddRuntimeEndpointFlow(props: AddRuntimeEndpointFlowProps) { - const [flow, setFlow] = useState({ name: 'loading' }); - - useEffect(() => { - void (async () => { - try { - const configIO = new ConfigIO(); - const project = await configIO.readProjectSpec(); - if (project.runtimes.length === 0) { - setFlow({ name: 'error', message: 'No runtimes found. Add an agent first with `agentcore add agent`.' }); - return; - } - setFlow({ name: 'create-wizard', runtimes: project.runtimes }); - } catch (err) { - setFlow({ name: 'error', message: err instanceof Error ? err.message : 'Unknown error' }); - } - })(); - }, []); - - const handleComplete = useCallback(async (config: RuntimeEndpointWizardConfig) => { - const result = await runtimeEndpointPrimitive.add({ - runtime: config.runtimeName, - endpoint: config.endpointName, - version: config.version, - description: config.description, - }); - if (result.success) { - setFlow({ name: 'create-success', endpointName: config.endpointName, runtimeName: config.runtimeName }); - } else { - setFlow({ name: 'error', message: result.error ?? 'Failed to add endpoint' }); - } - }, []); - - // Non-interactive: exit after success - useEffect(() => { - if (!props.isInteractive && flow.name === 'create-success') { - props.onExit(); - } - }, [props.isInteractive, flow.name, props.onExit]); - - if (flow.name === 'loading') { - return null; - } - - if (flow.name === 'create-wizard') { - return ( - void handleComplete(config)} - onBack={props.onBack} - onExit={props.onExit} - /> - ); - } - - if (flow.name === 'create-success') { - return ( - - ); - } - - return ( - - ); -} -``` - -- [ ] **Step 5: Create index.ts barrel** - -Create `agentcore-cli/src/cli/tui/screens/runtime-endpoint/index.ts`: - -```typescript -export { AddRuntimeEndpointFlow } from './AddRuntimeEndpointFlow'; -export { AddRuntimeEndpointScreen } from './AddRuntimeEndpointScreen'; -export type { RuntimeEndpointWizardConfig } from './types'; -``` - -- [ ] **Step 6: Add to AddScreen.tsx** - -In `agentcore-cli/src/cli/tui/screens/add/AddScreen.tsx`, add to `ADD_RESOURCES` array (after `'gateway-target'`): - -```typescript -{ id: 'runtime-endpoint', title: 'Runtime Endpoint', description: 'Named endpoint (version alias) for a runtime' }, -``` - -- [ ] **Step 7: Add to AddFlow.tsx** - -In `agentcore-cli/src/cli/tui/screens/add/AddFlow.tsx`: - -1. Add import: - -```typescript -import { AddRuntimeEndpointFlow } from '../runtime-endpoint'; -``` - -2. Add to `FlowState` union: - -```typescript -| { name: 'runtime-endpoint-wizard' } -``` - -3. Add to `getInitialFlowState`: - -```typescript -case 'runtime-endpoint': - return { name: 'runtime-endpoint-wizard' }; -``` - -4. Add to `handleSelectResource`: - -```typescript -case 'runtime-endpoint': - setFlow({ name: 'runtime-endpoint-wizard' }); - break; -``` - -5. Add render block (after the policy-wizard block): - -```typescript -if (flow.name === 'runtime-endpoint-wizard') { - return ( - setFlow({ name: 'select' })} - onDev={props.onDev} - onDeploy={props.onDeploy} - /> - ); -} -``` - -- [ ] **Step 8: Verify build passes** - -Run: `cd /Users/tjariy/github_workspace/runtime_endpoint_project/agentcore-cli && npx tsc --noEmit` Expected: No errors - -- [ ] **Step 9: Commit** - -```bash -cd /Users/tjariy/github_workspace/runtime_endpoint_project/agentcore-cli -git add src/cli/tui/screens/runtime-endpoint/ src/cli/tui/screens/add/AddScreen.tsx src/cli/tui/screens/add/AddFlow.tsx -git commit -m "feat: add runtime-endpoint TUI add flow" -``` - ---- - -## Task 7: Add runtime-endpoint to TUI Remove flow (CLI repo) - -**Files:** - -- Create: `agentcore-cli/src/cli/tui/screens/remove/RemoveRuntimeEndpointScreen.tsx` -- Modify: `agentcore-cli/src/cli/tui/screens/remove/RemoveFlow.tsx` -- Modify: `agentcore-cli/src/cli/tui/screens/remove/RemoveScreen.tsx` -- Modify: `agentcore-cli/src/cli/tui/hooks/useRemove.ts` - -- [ ] **Step 1: Add hooks to useRemove.ts** - -In `agentcore-cli/src/cli/tui/hooks/useRemove.ts`, add: - -```typescript -import type { RemovableRuntimeEndpoint } from '../../primitives/RuntimeEndpointPrimitive'; -import { runtimeEndpointPrimitive } from '../../primitives/registry'; - -export function useRemovableRuntimeEndpoints() { - const { items: endpoints, ...rest } = useRemovableResources(() => runtimeEndpointPrimitive.getRemovable()); - return { endpoints, ...rest }; -} - -export function useRemoveRuntimeEndpoint() { - return useRemoveResource( - async (name, preview) => runtimeEndpointPrimitive.remove(name), - 'runtime-endpoint', - name => name - ); -} -``` - -Also add a `loadRuntimeEndpointPreview` function to the `useRemovalPreview` hook: - -```typescript -const loadRuntimeEndpointPreview = useCallback(async (name: string) => { - try { - const preview = await runtimeEndpointPrimitive.previewRemove(name); - return { ok: true as const, preview }; - } catch (err) { - return { ok: false as const, error: err instanceof Error ? err.message : 'Unknown error' }; - } -}, []); -``` - -Return it from the hook alongside the others. - -- [ ] **Step 2: Create RemoveRuntimeEndpointScreen.tsx** - -Create `agentcore-cli/src/cli/tui/screens/remove/RemoveRuntimeEndpointScreen.tsx`: - -```typescript -import type { RemovableRuntimeEndpoint } from '../../../primitives/RuntimeEndpointPrimitive'; -import { SelectScreen } from '../../components'; -import React from 'react'; - -interface RemoveRuntimeEndpointScreenProps { - endpoints: RemovableRuntimeEndpoint[]; - onSelect: (name: string) => void; - onExit: () => void; -} - -export function RemoveRuntimeEndpointScreen({ endpoints, onSelect, onExit }: RemoveRuntimeEndpointScreenProps) { - if (endpoints.length === 0) { - return null; - } - - const items = endpoints.map(ep => ({ - id: ep.name, - title: `${ep.name}`, - description: `${ep.runtimeName} v${ep.version}${ep.description ? ` — ${ep.description}` : ''}`, - })); - - return ( - onSelect(item.id)} - onExit={onExit} - /> - ); -} -``` - -- [ ] **Step 3: Wire into RemoveFlow.tsx** - -In `agentcore-cli/src/cli/tui/screens/remove/RemoveFlow.tsx`: - -1. Add imports for the new hooks and screen. -2. Add flow states to the `FlowState` union: - -```typescript -| { name: 'select-runtime-endpoint' } -| { name: 'confirm-runtime-endpoint'; endpointName: string; preview: RemovalPreview } -| { name: 'runtime-endpoint-success'; endpointName: string; logFilePath?: string } -``` - -3. Add data hook: - `const { endpoints: runtimeEndpoints, isLoading: isLoadingRuntimeEndpoints, refresh: refreshRuntimeEndpoints } = useRemovableRuntimeEndpoints();` -4. Add to `isLoading` computation. -5. Add `handleSelectRuntimeEndpoint` callback (same pattern as others). -6. Add `handleConfirmRuntimeEndpoint` callback. -7. Add `case 'runtime-endpoint':` to `handleSelectResource` and `getInitialState`. -8. Add render blocks for `select-runtime-endpoint`, `confirm-runtime-endpoint`, and `runtime-endpoint-success`. -9. Add to `resetAll` and `refreshAll`. -10. Add to the `RemoveScreen` component's props (endpointCount). - -- [ ] **Step 4: Add to RemoveScreen.tsx** - -Add `'runtime-endpoint'` to the remove resources list with a count badge. - -- [ ] **Step 5: Verify build passes** - -Run: `cd /Users/tjariy/github_workspace/runtime_endpoint_project/agentcore-cli && npx tsc --noEmit` Expected: No errors - -- [ ] **Step 6: Commit** - -```bash -cd /Users/tjariy/github_workspace/runtime_endpoint_project/agentcore-cli -git add src/cli/tui/screens/remove/ src/cli/tui/hooks/useRemove.ts -git commit -m "feat: add runtime-endpoint TUI remove flow" -``` - ---- - -## Task 8: Display endpoints in status and ResourceGraph (CLI repo) - -**Files:** - -- Modify: `agentcore-cli/src/cli/tui/components/ResourceGraph.tsx` -- Modify: `agentcore-cli/src/cli/commands/status/action.ts` - -- [ ] **Step 1: Add endpoint display to ResourceGraph** - -In `agentcore-cli/src/cli/tui/components/ResourceGraph.tsx`, modify the Agents section to render endpoints nested under -each agent. After the `ResourceRow` for each agent, add: - -```typescript -{agent.endpoints && Object.entries(agent.endpoints).map(([epName, ep]) => ( - - {' '} - {epName} - {' '}v{ep.version} - {ep.description && {' '}{ep.description}} - -))} -``` - -- [ ] **Step 2: Update status action to include version in deployed state** - -In `agentcore-cli/src/cli/commands/status/action.ts`, in the `computeResourceStatuses` function, update the agents -section to include version info in the detail field when available: - -```typescript -const agents = diffResourceSet({ - resourceType: 'agent', - localItems: project.runtimes, - deployedRecord: resources?.runtimes ?? {}, - getIdentifier: deployed => deployed.runtimeArn, - getLocalDetail: item => { - const endpointCount = item.endpoints ? Object.keys(item.endpoints).length : 0; - return endpointCount > 0 ? `${endpointCount} endpoint${endpointCount !== 1 ? 's' : ''}` : undefined; - }, -}); -``` - -- [ ] **Step 3: Verify build passes** - -Run: `cd /Users/tjariy/github_workspace/runtime_endpoint_project/agentcore-cli && npx tsc --noEmit` Expected: No errors - -- [ ] **Step 4: Commit** - -```bash -cd /Users/tjariy/github_workspace/runtime_endpoint_project/agentcore-cli -git add src/cli/tui/components/ResourceGraph.tsx src/cli/commands/status/action.ts -git commit -m "feat: display runtime endpoints in status and resource graph" -``` - ---- - -## Task 9: Mirror schema changes in CDK repo - -**Files:** - -- Modify: `agentcore-l3-cdk-constructs/src/schema/schemas/agent-env.ts` -- Test: `agentcore-l3-cdk-constructs/test/schemas/agent-env.test.ts` - -- [ ] **Step 1: Write the failing test** - -Same test as Task 1 Step 1 but in the CDK repo's test directory. - -- [ ] **Step 2: Run test to verify it fails** - -Run: -`cd /Users/tjariy/github_workspace/runtime_endpoint_project/agentcore-l3-cdk-constructs && npx vitest run test/schemas/agent-env.test.ts --reporter=verbose` -Expected: FAIL - -- [ ] **Step 3: Add RuntimeEndpointSchema and endpoints field** - -Mirror the exact same schema changes from Task 1 Step 3 into -`agentcore-l3-cdk-constructs/src/schema/schemas/agent-env.ts`. The schemas must be identical across both repos. - -- [ ] **Step 4: Run test to verify it passes** - -Run: -`cd /Users/tjariy/github_workspace/runtime_endpoint_project/agentcore-l3-cdk-constructs && npx vitest run test/schemas/agent-env.test.ts --reporter=verbose` -Expected: PASS - -- [ ] **Step 5: Commit** - -```bash -cd /Users/tjariy/github_workspace/runtime_endpoint_project/agentcore-l3-cdk-constructs -git add src/schema/schemas/agent-env.ts test/schemas/agent-env.test.ts -git commit -m "feat: add endpoints field to AgentEnvSpec schema (CDK)" -``` - ---- - -## Task 10: Add CDK construct support for runtime endpoints - -**Files:** - -- Modify: `agentcore-l3-cdk-constructs/src/cdk/constructs/l3/AgentEnvironment.ts` -- Modify: `agentcore-l3-cdk-constructs/src/cdk/constructs/components/mcp/mcp-utils.ts` -- Test: `agentcore-l3-cdk-constructs/test/constructs/agent-environment.test.ts` - -**Important note:** There is no `CfnRuntimeEndpoint` L1 construct yet in `aws-cdk-lib`. The implementer must check -whether `aws_bedrockagentcore.CfnRuntimeEndpoint` exists in the version of `aws-cdk-lib` used (currently ^2.248.0). If -it does not exist: - -- Option A: Use `cdk.CfnResource` with resource type `AWS::BedrockAgentCore::RuntimeEndpoint` directly. -- Option B: Wait for the L1 to be generated and skip this task until then. - -The plan below assumes Option A (CfnResource) as a fallback. - -- [ ] **Step 1: Write the failing test** - -```typescript -describe('AgentEnvironment with endpoints', () => { - it('creates CfnResource for each endpoint in the spec', () => { - const stack = new cdk.Stack(); - const agent: AgentEnvSpec = { - ...validCodeZipAgent, - endpoints: { - prod: { version: 3, description: 'Production traffic' }, - staging: { version: 2 }, - }, - }; - - new AgentEnvironment(stack, 'TestEnv', { - projectName: 'TestProject', - agent, - configRoot: '/tmp/test', - }); - - const template = Template.fromStack(stack); - // Verify endpoint resources are created - template.resourceCountIs('AWS::BedrockAgentCore::RuntimeEndpoint', 2); - }); -}); -``` - -- [ ] **Step 2: Run test to verify it fails** - -Run: -`cd /Users/tjariy/github_workspace/runtime_endpoint_project/agentcore-l3-cdk-constructs && npx vitest run test/constructs/agent-environment.test.ts --reporter=verbose` -Expected: FAIL — no endpoint resources created - -- [ ] **Step 3: Add getRuntimeEndpointUrlWithQualifier to mcp-utils.ts** - -In `agentcore-l3-cdk-constructs/src/cdk/constructs/components/mcp/mcp-utils.ts`: - -```typescript -/** Runtime endpoint URL template with custom qualifier */ -export const RUNTIME_ENDPOINT_URL_WITH_QUALIFIER_TEMPLATE = - 'https://bedrock-agentcore.${AWS::Region}.${AWS::URLSuffix}/runtimes/${EncodedArn}/invocations?qualifier=${Qualifier}'; - -/** - * Generates a runtime endpoint URL with a specific qualifier (endpoint name). - */ -export function getRuntimeEndpointUrlWithQualifier(runtimeArn: string, qualifier: string): string { - const encodedArn = urlEncodeArn(runtimeArn); - return Fn.sub(RUNTIME_ENDPOINT_URL_WITH_QUALIFIER_TEMPLATE, { - EncodedArn: encodedArn, - Qualifier: qualifier, - }); -} -``` - -- [ ] **Step 4: Create endpoint resources in AgentEnvironment** - -In `agentcore-l3-cdk-constructs/src/cdk/constructs/l3/AgentEnvironment.ts`, after the runtime is created and env vars -are added, add endpoint creation: - -```typescript -// Create runtime endpoints (version aliases) -if (agent.endpoints) { - for (const [endpointName, endpointConfig] of Object.entries(agent.endpoints)) { - new cdk.CfnResource(this, `Endpoint${toPascalId(endpointName)}`, { - type: 'AWS::BedrockAgentCore::RuntimeEndpoint', - properties: { - AgentRuntimeId: this.runtime.runtimeId, - Name: endpointName, - AgentRuntimeVersion: String(endpointConfig.version), - ...(endpointConfig.description && { Description: endpointConfig.description }), - }, - }); - } -} -``` - -Add the import for `toPascalId` from `../../logical-ids` and `cdk` from `aws-cdk-lib`. - -- [ ] **Step 5: Run test to verify it passes** - -Run: -`cd /Users/tjariy/github_workspace/runtime_endpoint_project/agentcore-l3-cdk-constructs && npx vitest run test/constructs/agent-environment.test.ts --reporter=verbose` -Expected: PASS - -- [ ] **Step 6: Commit** - -```bash -cd /Users/tjariy/github_workspace/runtime_endpoint_project/agentcore-l3-cdk-constructs -git add src/cdk/constructs/l3/AgentEnvironment.ts src/cdk/constructs/components/mcp/mcp-utils.ts test/constructs/agent-environment.test.ts -git commit -m "feat: create CfnResource for runtime endpoints during CDK synth" -``` - ---- - -## Task 11: Update schema exports and run full test suite - -**Files:** - -- Modify: `agentcore-cli/src/schema/index.ts` (if not auto-exported) -- Modify: `agentcore-l3-cdk-constructs/src/schema/index.ts` (if not auto-exported) - -- [ ] **Step 1: Verify CLI schema exports RuntimeEndpoint types** - -Check `agentcore-cli/src/schema/index.ts` and ensure `RuntimeEndpoint`, `RuntimeEndpointSchema`, -`RuntimeEndpointNameSchema` are exported. Add if missing. - -- [ ] **Step 2: Verify CDK schema exports RuntimeEndpoint types** - -Check `agentcore-l3-cdk-constructs/src/schema/index.ts` (or `src/index.ts`) and ensure the same exports. Add if missing. - -- [ ] **Step 3: Run full CLI test suite** - -Run: `cd /Users/tjariy/github_workspace/runtime_endpoint_project/agentcore-cli && npm test` Expected: All tests pass. -Fix any snapshot failures with `npm run test:update-snapshots` if the asset files changed. - -- [ ] **Step 4: Run full CDK test suite** - -Run: `cd /Users/tjariy/github_workspace/runtime_endpoint_project/agentcore-l3-cdk-constructs && npm test` Expected: All -tests pass. - -- [ ] **Step 5: Commit any remaining fixes** - -```bash -cd /Users/tjariy/github_workspace/runtime_endpoint_project/agentcore-cli -git add -A && git commit -m "fix: update exports and fix test failures for runtime-endpoint feature" -cd /Users/tjariy/github_workspace/runtime_endpoint_project/agentcore-l3-cdk-constructs -git add -A && git commit -m "fix: update exports and fix test failures for runtime-endpoint feature" -``` - ---- - -## Notes for Implementer - -### Key decisions - -1. **Endpoints are a dictionary, not an array.** Keys are endpoint names, values have `version` and optional - `description`. This matches the spec: `"prod": { "version": 3, "description": "..." }`. -2. **No CfnRuntimeEndpoint L1 exists yet.** Task 10 uses `cdk.CfnResource` as a fallback. If/when the L1 is available, - switch to `bedrockagentcore.CfnRuntimeEndpoint`. -3. **Version defaults to 1** when not provided via CLI flag. The spec says "default to latest" — but "latest" requires - an API call to determine. For V1, default to 1 and document that users should check `agentcore status` for the - current version. -4. **Cross-reference validation** (gateway targets referencing endpoints) is stubbed out. The spec mentions this as - future work. The `findGatewayTargetReferences` method is ready for extension. -5. **Deploy behavior** is handled by CDK — no CLI deploy changes needed. The CDK stack reads `agentcore.json`, sees the - `endpoints` dict, and creates/updates the CFN resources. - -### Files NOT changed (intentionally) - -- `src/cli/commands/add/command.tsx` — subcommands auto-register via `primitive.registerCommands()` in `cli.ts`. -- `src/cli/commands/remove/command.tsx` — same auto-registration. -- `src/cli/cli.ts` — the `ALL_PRIMITIVES` loop handles registration automatically. -- `src/assets/cdk/` — the vended CDK project imports `@aws/agentcore-cdk` which contains the construct changes. From 11a7a8630f70cf930563b9738a44364429e52889 Mon Sep 17 00:00:00 2001 From: "T.J Ariyawansa" Date: Mon, 27 Apr 2026 17:12:43 -0400 Subject: [PATCH 12/18] fix: use composite key and parentName for endpoint identification - Add parentName field to ResourceStatusEntry for structured parent linking - Use runtimeName/endpointName composite key in remove/preview/getRemovable - Status command filters endpoints by parentName instead of parsing detail string - React keys use structured parentName/name instead of display strings --- src/cli/commands/status/action.ts | 10 ++- src/cli/commands/status/command.tsx | 10 +-- .../primitives/RuntimeEndpointPrimitive.ts | 70 ++++++++++++------- 3 files changed, 58 insertions(+), 32 deletions(-) diff --git a/src/cli/commands/status/action.ts b/src/cli/commands/status/action.ts index cb666cfd2..11d535e3e 100644 --- a/src/cli/commands/status/action.ts +++ b/src/cli/commands/status/action.ts @@ -24,6 +24,7 @@ export interface ResourceStatusEntry { deploymentState: ResourceDeploymentState; identifier?: string; detail?: string; + parentName?: string; error?: string; invocationUrl?: string; } @@ -80,6 +81,7 @@ function diffResourceSet({ getIdentifier, getLocalDetail, getDeployedKey, + getParentName, }: { resourceType: ResourceStatusEntry['resourceType']; localItems: TLocal[]; @@ -87,6 +89,7 @@ function diffResourceSet({ getIdentifier: (deployed: TDeployed) => string | undefined; getLocalDetail?: (item: TLocal) => string | undefined; getDeployedKey?: (item: TLocal) => string; + getParentName?: (item: TLocal) => string | undefined; }): ResourceStatusEntry[] { const entries: ResourceStatusEntry[] = []; const localKeys = new Set(localItems.map(item => (getDeployedKey ? getDeployedKey(item) : item.name))); @@ -100,16 +103,20 @@ function diffResourceSet({ deploymentState: deployed ? 'deployed' : 'local-only', identifier: deployed ? getIdentifier(deployed) : undefined, detail: getLocalDetail?.(item), + parentName: getParentName?.(item), }); } for (const [name, deployed] of Object.entries(deployedRecord)) { if (!localKeys.has(name)) { + // For pending-removal entries, try to extract parentName from composite key + const slashIdx = name.indexOf('/'); entries.push({ resourceType, name, deploymentState: 'pending-removal', identifier: getIdentifier(deployed), + parentName: getParentName && slashIdx > 0 ? name.substring(0, slashIdx) : undefined, }); } } @@ -223,8 +230,9 @@ export function computeResourceStatuses( localItems: localEndpoints, deployedRecord: resources?.runtimeEndpoints ?? {}, getIdentifier: deployed => deployed.endpointArn, - getLocalDetail: item => `${item.agentName} v${item.version}${item.description ? ` — ${item.description}` : ''}`, + getLocalDetail: item => `v${item.version}${item.description ? ` — ${item.description}` : ''}`, getDeployedKey: item => `${item.agentName}/${item.name}`, + getParentName: item => item.agentName, }); return [ diff --git a/src/cli/commands/status/command.tsx b/src/cli/commands/status/command.tsx index 8ad84d0a1..76c4580ea 100644 --- a/src/cli/commands/status/command.tsx +++ b/src/cli/commands/status/command.tsx @@ -157,7 +157,7 @@ export const registerStatus = (program: Command) => { Agents {agents.map(entry => { // Find endpoints belonging to this agent - const agentEndpoints = runtimeEndpoints.filter(ep => ep.detail?.startsWith(`${entry.name} `)); + const agentEndpoints = runtimeEndpoints.filter(ep => ep.parentName === entry.name); return ( @@ -167,8 +167,8 @@ export const registerStatus = (program: Command) => { )} {agentEndpoints.map(ep => ( - - {' '}◉ {ep.name} {ep.detail?.replace(`${entry.name} `, '')}{' '} + + {' '}◉ {ep.name} {ep.detail}{' '} [{DEPLOYMENT_STATE_LABELS[ep.deploymentState] ?? ep.deploymentState}] @@ -184,8 +184,8 @@ export const registerStatus = (program: Command) => { Runtime Endpoints {runtimeEndpoints.map(ep => ( - - {' '}◉ {ep.name} {ep.detail}{' '} + + {' '}◉ {ep.parentName}/{ep.name} {ep.detail}{' '} [{DEPLOYMENT_STATE_LABELS[ep.deploymentState] ?? ep.deploymentState}] diff --git a/src/cli/primitives/RuntimeEndpointPrimitive.ts b/src/cli/primitives/RuntimeEndpointPrimitive.ts index 056fa1da4..258dcffc1 100644 --- a/src/cli/primitives/RuntimeEndpointPrimitive.ts +++ b/src/cli/primitives/RuntimeEndpointPrimitive.ts @@ -49,9 +49,7 @@ export class RuntimeEndpointPrimitive extends BasePrimitive 0) { + const runtimeName = name.substring(0, slashIndex); + const endpointName = name.substring(slashIndex + 1); + const runtime = project.runtimes.find(r => r.name === runtimeName); + if (!runtime?.endpoints?.[endpointName]) { + return { success: false, error: `Runtime endpoint "${name}" not found.` }; + } + delete runtime.endpoints[endpointName]; + if (Object.keys(runtime.endpoints).length === 0) { + delete runtime.endpoints; + } + await this.writeProjectSpec(project); + return { success: true }; + } + + // Legacy: bare endpoint name — search all runtimes for (const runtime of project.runtimes) { - if (runtime.endpoints && runtime.endpoints[name]) { + if (runtime.endpoints?.[name]) { delete runtime.endpoints[name]; - - // Clean up empty endpoints dictionary if (Object.keys(runtime.endpoints).length === 0) { delete runtime.endpoints; } - - found = true; - break; + await this.writeProjectSpec(project); + return { success: true }; } } - if (!found) { - return { success: false, error: `Runtime endpoint "${name}" not found.` }; - } - - await this.writeProjectSpec(project); - return { success: true }; + return { success: false, error: `Runtime endpoint "${name}" not found.` }; } catch (err) { return { success: false, error: getErrorMessage(err) }; } @@ -144,15 +150,27 @@ export class RuntimeEndpointPrimitive extends BasePrimitive { const project = await this.readProjectSpec(); - // Find the runtime that contains this endpoint + // Support composite key: runtimeName/endpointName let runtimeName: string | undefined; + let endpointName: string = name; let endpointConfig: { version: number; description?: string } | undefined; - for (const runtime of project.runtimes) { - if (runtime.endpoints && runtime.endpoints[name]) { - runtimeName = runtime.name; - endpointConfig = runtime.endpoints[name]; - break; + const slashIndex = name.indexOf('/'); + if (slashIndex > 0) { + runtimeName = name.substring(0, slashIndex); + endpointName = name.substring(slashIndex + 1); + const runtime = project.runtimes.find(r => r.name === runtimeName); + if (runtime?.endpoints?.[endpointName]) { + endpointConfig = runtime.endpoints[endpointName]; + } + } else { + // Legacy: bare endpoint name — search all runtimes + for (const runtime of project.runtimes) { + if (runtime.endpoints?.[name]) { + runtimeName = runtime.name; + endpointConfig = runtime.endpoints[name]; + break; + } } } @@ -163,7 +181,7 @@ export class RuntimeEndpointPrimitive extends BasePrimitive a.name === runtimeName); if (afterRuntime?.endpoints) { - delete afterRuntime.endpoints[name]; + delete afterRuntime.endpoints[endpointName]; if (Object.keys(afterRuntime.endpoints).length === 0) { delete afterRuntime.endpoints; } @@ -198,7 +216,7 @@ export class RuntimeEndpointPrimitive extends BasePrimitive Date: Mon, 27 Apr 2026 17:18:06 -0400 Subject: [PATCH 13/18] test: add comprehensive unit tests for RuntimeEndpointPrimitive 23 tests covering add(), remove(), previewRemove(), getRemovable(): - Runtime lookup, duplicate detection, version validation - Composite key removal targeting correct runtime - Empty endpoints dict cleanup - Version validation against deployed state - Richer JSON response shape --- .../RuntimeEndpointPrimitive.test.ts | 358 +++++++++++++++++- 1 file changed, 355 insertions(+), 3 deletions(-) diff --git a/src/cli/primitives/__tests__/RuntimeEndpointPrimitive.test.ts b/src/cli/primitives/__tests__/RuntimeEndpointPrimitive.test.ts index e99ecf0b6..6da50fa78 100644 --- a/src/cli/primitives/__tests__/RuntimeEndpointPrimitive.test.ts +++ b/src/cli/primitives/__tests__/RuntimeEndpointPrimitive.test.ts @@ -1,8 +1,52 @@ -import { RuntimeEndpointPrimitive } from '../RuntimeEndpointPrimitive'; -import { describe, expect, it } from 'vitest'; +import { RuntimeEndpointPrimitive } from '../RuntimeEndpointPrimitive.js'; +import { afterEach, describe, expect, it, vi } from 'vitest'; + +const mockReadProjectSpec = vi.fn(); +const mockWriteProjectSpec = vi.fn(); +const mockConfigExists = vi.fn(); +const mockReadDeployedState = vi.fn(); + +vi.mock('../../../lib/index.js', () => ({ + ConfigIO: class { + readProjectSpec = mockReadProjectSpec; + writeProjectSpec = mockWriteProjectSpec; + configExists = mockConfigExists; + readDeployedState = mockReadDeployedState; + }, + findConfigRoot: () => '/fake/root', +})); + +function makeProject( + runtimes: Array<{ + name: string; + endpoints?: Record; + }> = [] +) { + return { + name: 'TestProject', + version: 1, + managedBy: 'CDK' as const, + runtimes: runtimes.map(r => ({ + name: r.name, + build: 'CodeZip' as const, + entrypoint: 'main.py' as any, + codeLocation: `app/${r.name}/` as any, + runtimeVersion: 'PYTHON_3_14' as any, + ...(r.endpoints && { endpoints: r.endpoints }), + })), + memories: [], + credentials: [], + evaluators: [], + onlineEvalConfigs: [], + agentCoreGateways: [], + policyEngines: [], + }; +} + +const primitive = new RuntimeEndpointPrimitive(); describe('RuntimeEndpointPrimitive', () => { - const primitive = new RuntimeEndpointPrimitive(); + afterEach(() => vi.clearAllMocks()); it('has kind "runtime-endpoint"', () => { expect(primitive.kind).toBe('runtime-endpoint'); @@ -11,4 +55,312 @@ describe('RuntimeEndpointPrimitive', () => { it('has label "Runtime Endpoint"', () => { expect(primitive.label).toBe('Runtime Endpoint'); }); + + describe('add', () => { + it('successfully adds endpoint to a runtime', async () => { + mockReadProjectSpec.mockResolvedValue(makeProject([{ name: 'MyRuntime' }])); + mockWriteProjectSpec.mockResolvedValue(undefined); + mockConfigExists.mockReturnValue(false); + + const result = await primitive.add({ + runtime: 'MyRuntime', + endpoint: 'prod', + version: 3, + description: 'Production endpoint', + }); + + expect(result.success).toBe(true); + + const writtenSpec = mockWriteProjectSpec.mock.calls[0]![0]; + const runtime = writtenSpec.runtimes.find((r: any) => r.name === 'MyRuntime'); + expect(runtime.endpoints).toHaveProperty('prod'); + expect(runtime.endpoints.prod.version).toBe(3); + expect(runtime.endpoints.prod.description).toBe('Production endpoint'); + }); + + it('returns error when runtime not found', async () => { + mockReadProjectSpec.mockResolvedValue(makeProject([{ name: 'OtherRuntime' }])); + mockConfigExists.mockReturnValue(false); + + const result = await primitive.add({ + runtime: 'NonExistent', + endpoint: 'prod', + }); + + expect(result).toEqual( + expect.objectContaining({ success: false, error: expect.stringContaining('not found') }) + ); + }); + + it('returns error when endpoint already exists', async () => { + mockReadProjectSpec.mockResolvedValue( + makeProject([{ name: 'MyRuntime', endpoints: { prod: { version: 1 } } }]) + ); + mockConfigExists.mockReturnValue(false); + + const result = await primitive.add({ + runtime: 'MyRuntime', + endpoint: 'prod', + }); + + expect(result).toEqual( + expect.objectContaining({ success: false, error: expect.stringContaining('already exists') }) + ); + }); + + it('defaults version to 1 when not provided', async () => { + mockReadProjectSpec.mockResolvedValue(makeProject([{ name: 'MyRuntime' }])); + mockWriteProjectSpec.mockResolvedValue(undefined); + mockConfigExists.mockReturnValue(false); + + const result = await primitive.add({ + runtime: 'MyRuntime', + endpoint: 'staging', + }); + + expect(result.success).toBe(true); + const writtenSpec = mockWriteProjectSpec.mock.calls[0]![0]; + expect(writtenSpec.runtimes[0].endpoints.staging.version).toBe(1); + }); + + it.each([ + { version: 0, label: 'zero' }, + { version: -1, label: 'negative' }, + { version: 3.5, label: 'non-integer' }, + ])('returns error when version is $label ($version)', async ({ version }) => { + mockReadProjectSpec.mockResolvedValue(makeProject([{ name: 'MyRuntime' }])); + mockConfigExists.mockReturnValue(false); + + const result = await primitive.add({ + runtime: 'MyRuntime', + endpoint: 'prod', + version, + }); + + expect(result).toEqual( + expect.objectContaining({ success: false, error: expect.stringContaining('positive integer') }) + ); + }); + + it('returns richer JSON response with endpointName, agent, and version', async () => { + mockReadProjectSpec.mockResolvedValue(makeProject([{ name: 'MyRuntime' }])); + mockWriteProjectSpec.mockResolvedValue(undefined); + mockConfigExists.mockReturnValue(false); + + const result = await primitive.add({ + runtime: 'MyRuntime', + endpoint: 'prod', + version: 2, + }); + + expect(result).toEqual( + expect.objectContaining({ + success: true, + endpointName: 'prod', + agent: 'MyRuntime', + version: 2, + }) + ); + }); + + it('returns error when version exceeds latest deployed version', async () => { + mockReadProjectSpec.mockResolvedValue(makeProject([{ name: 'MyRuntime' }])); + mockConfigExists.mockReturnValue(true); + mockReadDeployedState.mockResolvedValue({ + targets: { + 'us-east-1': { + resources: { + runtimes: { + MyRuntime: { runtimeVersion: 3 }, + }, + }, + }, + }, + }); + + const result = await primitive.add({ + runtime: 'MyRuntime', + endpoint: 'prod', + version: 5, + }); + + expect(result).toEqual( + expect.objectContaining({ success: false, error: expect.stringContaining('exceeds latest deployed version') }) + ); + }); + }); + + describe('remove', () => { + it('removes endpoint using composite key runtimeName/endpointName', async () => { + mockReadProjectSpec.mockResolvedValue( + makeProject([ + { + name: 'MyRuntime', + endpoints: { prod: { version: 1 }, staging: { version: 2 } }, + }, + ]) + ); + mockWriteProjectSpec.mockResolvedValue(undefined); + + const result = await primitive.remove('MyRuntime/prod'); + + expect(result.success).toBe(true); + const writtenSpec = mockWriteProjectSpec.mock.calls[0]![0]; + const runtime = writtenSpec.runtimes[0]; + expect(runtime.endpoints).not.toHaveProperty('prod'); + expect(runtime.endpoints).toHaveProperty('staging'); + }); + + it('removes endpoint using legacy bare name (fallback)', async () => { + mockReadProjectSpec.mockResolvedValue( + makeProject([{ name: 'MyRuntime', endpoints: { prod: { version: 1 } } }]) + ); + mockWriteProjectSpec.mockResolvedValue(undefined); + + const result = await primitive.remove('prod'); + + expect(result.success).toBe(true); + const writtenSpec = mockWriteProjectSpec.mock.calls[0]![0]; + expect(writtenSpec.runtimes[0].endpoints).toBeUndefined(); + }); + + it('returns error when endpoint not found', async () => { + mockReadProjectSpec.mockResolvedValue(makeProject([{ name: 'MyRuntime' }])); + + const result = await primitive.remove('MyRuntime/nonexistent'); + + expect(result).toEqual( + expect.objectContaining({ success: false, error: expect.stringContaining('not found') }) + ); + }); + + it('cleans up empty endpoints dict after removing last endpoint', async () => { + mockReadProjectSpec.mockResolvedValue( + makeProject([{ name: 'MyRuntime', endpoints: { prod: { version: 1 } } }]) + ); + mockWriteProjectSpec.mockResolvedValue(undefined); + + const result = await primitive.remove('MyRuntime/prod'); + + expect(result.success).toBe(true); + const writtenSpec = mockWriteProjectSpec.mock.calls[0]![0]; + expect(writtenSpec.runtimes[0].endpoints).toBeUndefined(); + }); + + it('correctly targets the right runtime when same endpoint name exists on multiple runtimes', async () => { + mockReadProjectSpec.mockResolvedValue( + makeProject([ + { name: 'RuntimeA', endpoints: { prod: { version: 1 } } }, + { name: 'RuntimeB', endpoints: { prod: { version: 2 } } }, + ]) + ); + mockWriteProjectSpec.mockResolvedValue(undefined); + + const result = await primitive.remove('RuntimeB/prod'); + + expect(result.success).toBe(true); + const writtenSpec = mockWriteProjectSpec.mock.calls[0]![0]; + // RuntimeA should still have its prod endpoint + expect(writtenSpec.runtimes[0].endpoints).toHaveProperty('prod'); + // RuntimeB should have had its prod endpoint removed + expect(writtenSpec.runtimes[1].endpoints).toBeUndefined(); + }); + }); + + describe('previewRemove', () => { + it('returns summary with correct runtime and endpoint info using composite key', async () => { + mockReadProjectSpec.mockResolvedValue( + makeProject([ + { + name: 'MyRuntime', + endpoints: { prod: { version: 3, description: 'Production' } }, + }, + ]) + ); + + const preview = await primitive.previewRemove('MyRuntime/prod'); + + expect(preview.summary).toEqual( + expect.arrayContaining([ + expect.stringContaining('prod'), + expect.stringContaining('MyRuntime'), + ]) + ); + expect(preview.summary).toEqual(expect.arrayContaining([expect.stringContaining('Version: 3')])); + expect(preview.summary).toEqual(expect.arrayContaining([expect.stringContaining('Production')])); + }); + + it('returns schemaChanges showing before/after agentcore.json', async () => { + mockReadProjectSpec.mockResolvedValue( + makeProject([{ name: 'MyRuntime', endpoints: { prod: { version: 1 } } }]) + ); + + const preview = await primitive.previewRemove('MyRuntime/prod'); + + expect(preview.schemaChanges).toHaveLength(1); + expect(preview.schemaChanges[0]!.file).toBe('agentcore/agentcore.json'); + + // Before should have the endpoint + const before = preview.schemaChanges[0]!.before as any; + expect(before.runtimes[0].endpoints).toHaveProperty('prod'); + + // After should not have the endpoint (and endpoints dict cleaned up) + const after = preview.schemaChanges[0]!.after as any; + expect(after.runtimes[0].endpoints).toBeUndefined(); + }); + + it('throws when endpoint not found', async () => { + mockReadProjectSpec.mockResolvedValue(makeProject([{ name: 'MyRuntime' }])); + + await expect(primitive.previewRemove('MyRuntime/missing')).rejects.toThrow('not found'); + }); + }); + + describe('getRemovable', () => { + it('returns all endpoints across all runtimes', async () => { + mockReadProjectSpec.mockResolvedValue( + makeProject([ + { name: 'RuntimeA', endpoints: { prod: { version: 1 }, staging: { version: 2 } } }, + { name: 'RuntimeB', endpoints: { beta: { version: 3 } } }, + ]) + ); + + const result = await primitive.getRemovable(); + + expect(result).toHaveLength(3); + expect(result).toEqual( + expect.arrayContaining([ + expect.objectContaining({ name: 'RuntimeA/prod', runtimeName: 'RuntimeA', endpointName: 'prod', version: 1 }), + expect.objectContaining({ name: 'RuntimeA/staging', runtimeName: 'RuntimeA', endpointName: 'staging', version: 2 }), + expect.objectContaining({ name: 'RuntimeB/beta', runtimeName: 'RuntimeB', endpointName: 'beta', version: 3 }), + ]) + ); + }); + + it('uses composite key format runtimeName/endpointName for name field', async () => { + mockReadProjectSpec.mockResolvedValue( + makeProject([{ name: 'MyRuntime', endpoints: { prod: { version: 1 } } }]) + ); + + const result = await primitive.getRemovable(); + + expect(result[0]!.name).toBe('MyRuntime/prod'); + }); + + it('returns empty array when no endpoints exist', async () => { + mockReadProjectSpec.mockResolvedValue(makeProject([{ name: 'MyRuntime' }])); + + const result = await primitive.getRemovable(); + + expect(result).toEqual([]); + }); + + it('returns empty array when no runtimes exist', async () => { + mockReadProjectSpec.mockResolvedValue(makeProject()); + + const result = await primitive.getRemovable(); + + expect(result).toEqual([]); + }); + }); }); From b9f158f2b26aa16bd2524bf029339d47c0472d7d Mon Sep 17 00:00:00 2001 From: "T.J Ariyawansa" Date: Mon, 27 Apr 2026 17:20:20 -0400 Subject: [PATCH 14/18] fix: remove dead findGatewayTargetReferences stub --- src/cli/primitives/RuntimeEndpointPrimitive.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/cli/primitives/RuntimeEndpointPrimitive.ts b/src/cli/primitives/RuntimeEndpointPrimitive.ts index 258dcffc1..2eede74fc 100644 --- a/src/cli/primitives/RuntimeEndpointPrimitive.ts +++ b/src/cli/primitives/RuntimeEndpointPrimitive.ts @@ -352,11 +352,4 @@ export class RuntimeEndpointPrimitive extends BasePrimitive Date: Mon, 27 Apr 2026 17:22:12 -0400 Subject: [PATCH 15/18] fix: use BasePrimitive configIO instead of ad-hoc ConfigIO in add() --- src/cli/primitives/RuntimeEndpointPrimitive.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/cli/primitives/RuntimeEndpointPrimitive.ts b/src/cli/primitives/RuntimeEndpointPrimitive.ts index 2eede74fc..358b5fa69 100644 --- a/src/cli/primitives/RuntimeEndpointPrimitive.ts +++ b/src/cli/primitives/RuntimeEndpointPrimitive.ts @@ -1,4 +1,4 @@ -import { ConfigIO, findConfigRoot } from '../../lib'; +import { findConfigRoot } from '../../lib'; import type { AgentCoreProjectSpec } from '../../schema'; import { RuntimeEndpointSchema } from '../../schema'; import type { ResourceType } from '../commands/remove/types'; @@ -67,9 +67,8 @@ export class RuntimeEndpointPrimitive extends BasePrimitive deployedRuntime.runtimeVersion) { From be47f358da05ba83e6aca5c5fd7c366001dd47b2 Mon Sep 17 00:00:00 2001 From: "T.J Ariyawansa" Date: Mon, 27 Apr 2026 17:23:03 -0400 Subject: [PATCH 16/18] fix: use Number() instead of parseInt in TUI version validation --- .../screens/runtime-endpoint/AddRuntimeEndpointScreen.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/cli/tui/screens/runtime-endpoint/AddRuntimeEndpointScreen.tsx b/src/cli/tui/screens/runtime-endpoint/AddRuntimeEndpointScreen.tsx index f2d3ee42c..26bdb2d93 100644 --- a/src/cli/tui/screens/runtime-endpoint/AddRuntimeEndpointScreen.tsx +++ b/src/cli/tui/screens/runtime-endpoint/AddRuntimeEndpointScreen.tsx @@ -121,8 +121,8 @@ export function AddRuntimeEndpointScreen({ } } if (activeField === 'version') { - const num = parseInt(endpointVersion, 10); - if (isNaN(num) || num < 1) { + const num = Number(endpointVersion); + if (!Number.isInteger(num) || num < 1) { setError('Version must be a positive integer'); return; } @@ -144,8 +144,8 @@ export function AddRuntimeEndpointScreen({ setError('Must begin with a letter, alphanumeric + underscores only (max 48 chars)'); return; } - const ver = parseInt(endpointVersion, 10); - if (isNaN(ver) || ver < 1) { + const ver = Number(endpointVersion); + if (!Number.isInteger(ver) || ver < 1) { setError('Version must be a positive integer'); return; } From ddbeff20e550d71464d1c4645a0957c1c2660444 Mon Sep 17 00:00:00 2001 From: "T.J Ariyawansa" Date: Mon, 27 Apr 2026 17:43:20 -0400 Subject: [PATCH 17/18] chore: fix prettier formatting --- .../RuntimeEndpointPrimitive.test.ts | 40 +++++++------------ .../AddRuntimeEndpointScreen.tsx | 7 ++-- 2 files changed, 17 insertions(+), 30 deletions(-) diff --git a/src/cli/primitives/__tests__/RuntimeEndpointPrimitive.test.ts b/src/cli/primitives/__tests__/RuntimeEndpointPrimitive.test.ts index 6da50fa78..da1199723 100644 --- a/src/cli/primitives/__tests__/RuntimeEndpointPrimitive.test.ts +++ b/src/cli/primitives/__tests__/RuntimeEndpointPrimitive.test.ts @@ -87,15 +87,11 @@ describe('RuntimeEndpointPrimitive', () => { endpoint: 'prod', }); - expect(result).toEqual( - expect.objectContaining({ success: false, error: expect.stringContaining('not found') }) - ); + expect(result).toEqual(expect.objectContaining({ success: false, error: expect.stringContaining('not found') })); }); it('returns error when endpoint already exists', async () => { - mockReadProjectSpec.mockResolvedValue( - makeProject([{ name: 'MyRuntime', endpoints: { prod: { version: 1 } } }]) - ); + mockReadProjectSpec.mockResolvedValue(makeProject([{ name: 'MyRuntime', endpoints: { prod: { version: 1 } } }])); mockConfigExists.mockReturnValue(false); const result = await primitive.add({ @@ -212,9 +208,7 @@ describe('RuntimeEndpointPrimitive', () => { }); it('removes endpoint using legacy bare name (fallback)', async () => { - mockReadProjectSpec.mockResolvedValue( - makeProject([{ name: 'MyRuntime', endpoints: { prod: { version: 1 } } }]) - ); + mockReadProjectSpec.mockResolvedValue(makeProject([{ name: 'MyRuntime', endpoints: { prod: { version: 1 } } }])); mockWriteProjectSpec.mockResolvedValue(undefined); const result = await primitive.remove('prod'); @@ -229,15 +223,11 @@ describe('RuntimeEndpointPrimitive', () => { const result = await primitive.remove('MyRuntime/nonexistent'); - expect(result).toEqual( - expect.objectContaining({ success: false, error: expect.stringContaining('not found') }) - ); + expect(result).toEqual(expect.objectContaining({ success: false, error: expect.stringContaining('not found') })); }); it('cleans up empty endpoints dict after removing last endpoint', async () => { - mockReadProjectSpec.mockResolvedValue( - makeProject([{ name: 'MyRuntime', endpoints: { prod: { version: 1 } } }]) - ); + mockReadProjectSpec.mockResolvedValue(makeProject([{ name: 'MyRuntime', endpoints: { prod: { version: 1 } } }])); mockWriteProjectSpec.mockResolvedValue(undefined); const result = await primitive.remove('MyRuntime/prod'); @@ -281,19 +271,14 @@ describe('RuntimeEndpointPrimitive', () => { const preview = await primitive.previewRemove('MyRuntime/prod'); expect(preview.summary).toEqual( - expect.arrayContaining([ - expect.stringContaining('prod'), - expect.stringContaining('MyRuntime'), - ]) + expect.arrayContaining([expect.stringContaining('prod'), expect.stringContaining('MyRuntime')]) ); expect(preview.summary).toEqual(expect.arrayContaining([expect.stringContaining('Version: 3')])); expect(preview.summary).toEqual(expect.arrayContaining([expect.stringContaining('Production')])); }); it('returns schemaChanges showing before/after agentcore.json', async () => { - mockReadProjectSpec.mockResolvedValue( - makeProject([{ name: 'MyRuntime', endpoints: { prod: { version: 1 } } }]) - ); + mockReadProjectSpec.mockResolvedValue(makeProject([{ name: 'MyRuntime', endpoints: { prod: { version: 1 } } }])); const preview = await primitive.previewRemove('MyRuntime/prod'); @@ -331,16 +316,19 @@ describe('RuntimeEndpointPrimitive', () => { expect(result).toEqual( expect.arrayContaining([ expect.objectContaining({ name: 'RuntimeA/prod', runtimeName: 'RuntimeA', endpointName: 'prod', version: 1 }), - expect.objectContaining({ name: 'RuntimeA/staging', runtimeName: 'RuntimeA', endpointName: 'staging', version: 2 }), + expect.objectContaining({ + name: 'RuntimeA/staging', + runtimeName: 'RuntimeA', + endpointName: 'staging', + version: 2, + }), expect.objectContaining({ name: 'RuntimeB/beta', runtimeName: 'RuntimeB', endpointName: 'beta', version: 3 }), ]) ); }); it('uses composite key format runtimeName/endpointName for name field', async () => { - mockReadProjectSpec.mockResolvedValue( - makeProject([{ name: 'MyRuntime', endpoints: { prod: { version: 1 } } }]) - ); + mockReadProjectSpec.mockResolvedValue(makeProject([{ name: 'MyRuntime', endpoints: { prod: { version: 1 } } }])); const result = await primitive.getRemovable(); diff --git a/src/cli/tui/screens/runtime-endpoint/AddRuntimeEndpointScreen.tsx b/src/cli/tui/screens/runtime-endpoint/AddRuntimeEndpointScreen.tsx index 26bdb2d93..44c884ad0 100644 --- a/src/cli/tui/screens/runtime-endpoint/AddRuntimeEndpointScreen.tsx +++ b/src/cli/tui/screens/runtime-endpoint/AddRuntimeEndpointScreen.tsx @@ -221,9 +221,7 @@ export function AddRuntimeEndpointScreen({ {isEndpointStep && ( Runtime: {effectiveConfig.runtimeName} - {isDeployed && ( - Current deployed version: {maxVersion} - )} + {isDeployed && Current deployed version: {maxVersion}} Endpoint name: @@ -236,7 +234,8 @@ export function AddRuntimeEndpointScreen({ - Version{isDeployed ? ` (1-${maxVersion})` : ''}: + Version{isDeployed ? ` (1-${maxVersion})` : ''}:{' '} + {activeField === 'version' && !endpointVersion && } {endpointVersion || 1} From 117a07e8554e7683d0274c9f372cf9a07d1a0ec7 Mon Sep 17 00:00:00 2001 From: "T.J Ariyawansa" Date: Mon, 27 Apr 2026 17:47:00 -0400 Subject: [PATCH 18/18] fix: use T[] instead of Array to satisfy eslint array-type rule --- src/cli/primitives/__tests__/RuntimeEndpointPrimitive.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cli/primitives/__tests__/RuntimeEndpointPrimitive.test.ts b/src/cli/primitives/__tests__/RuntimeEndpointPrimitive.test.ts index da1199723..46fe426f5 100644 --- a/src/cli/primitives/__tests__/RuntimeEndpointPrimitive.test.ts +++ b/src/cli/primitives/__tests__/RuntimeEndpointPrimitive.test.ts @@ -17,10 +17,10 @@ vi.mock('../../../lib/index.js', () => ({ })); function makeProject( - runtimes: Array<{ + runtimes: { name: string; endpoints?: Record; - }> = [] + }[] = [] ) { return { name: 'TestProject',