Skip to content

Commit 41c59ef

Browse files
authored
feat: runtime endpoint support in AgentCore CLI (#979)
* 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 * 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. * fix: remove .omc state files and unused useCallback import - Remove .omc/ from git tracking, add to .gitignore - Remove unused useCallback import in AddRuntimeEndpointScreen.tsx * 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". * 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 * 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. * 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). * 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. * 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. * 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 * chore: remove planning and bug bash docs from PR * 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 * 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 * fix: remove dead findGatewayTargetReferences stub * fix: use BasePrimitive configIO instead of ad-hoc ConfigIO in add() * fix: use Number() instead of parseInt in TUI version validation * chore: fix prettier formatting * fix: use T[] instead of Array<T> to satisfy eslint array-type rule
1 parent 51240ac commit 41c59ef

29 files changed

Lines changed: 1740 additions & 23 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ ProtocolTesting/
6767

6868
# Auto-cloned CDK constructs (from scripts/bundle.mjs)
6969
.cdk-constructs-clone/
70+
.omc/
7071

7172
# Browser tests
7273
browser-tests/.browser-test-env

src/cli/cloudformation/outputs.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type {
66
OnlineEvalDeployedState,
77
PolicyDeployedState,
88
PolicyEngineDeployedState,
9+
RuntimeEndpointDeployedState,
910
TargetDeployedState,
1011
} from '../../schema';
1112
import { getCredentialProvider } from '../aws';
@@ -338,6 +339,40 @@ export function parsePolicyOutputs(
338339
return policies;
339340
}
340341

342+
/**
343+
* Parse stack outputs into deployed state for runtime endpoints.
344+
*
345+
* Output key pattern: ApplicationAgent{AgentPascal}Endpoint{AgentPascal}{EndpointPascal}(Id|Arn)Output{Hash}
346+
* The Agent{PascalName} prefix comes from the AgentEnvironment construct in the CDK tree.
347+
*/
348+
export function parseRuntimeEndpointOutputs(
349+
outputs: StackOutputs,
350+
endpointSpecs: { agentName: string; endpointName: string }[]
351+
): Record<string, RuntimeEndpointDeployedState> {
352+
const endpoints: Record<string, RuntimeEndpointDeployedState> = {};
353+
const outputKeys = Object.keys(outputs);
354+
355+
for (const { agentName, endpointName } of endpointSpecs) {
356+
const agentPascal = toPascalId(agentName);
357+
const endpointPascal = toPascalId('Endpoint', agentName, endpointName);
358+
const idPrefix = `ApplicationAgent${agentPascal}${endpointPascal}IdOutput`;
359+
const arnPrefix = `ApplicationAgent${agentPascal}${endpointPascal}ArnOutput`;
360+
361+
const idKey = outputKeys.find(k => k.startsWith(idPrefix));
362+
const arnKey = outputKeys.find(k => k.startsWith(arnPrefix));
363+
364+
if (idKey && arnKey) {
365+
const key = `${agentName}/${endpointName}`;
366+
endpoints[key] = {
367+
endpointId: outputs[idKey]!,
368+
endpointArn: outputs[arnKey]!,
369+
};
370+
}
371+
}
372+
373+
return endpoints;
374+
}
375+
341376
export interface BuildDeployedStateOptions {
342377
targetName: string;
343378
stackName: string;
@@ -351,6 +386,7 @@ export interface BuildDeployedStateOptions {
351386
onlineEvalConfigs?: Record<string, OnlineEvalDeployedState>;
352387
policyEngines?: Record<string, PolicyEngineDeployedState>;
353388
policies?: Record<string, PolicyDeployedState>;
389+
runtimeEndpoints?: Record<string, RuntimeEndpointDeployedState>;
354390
}
355391

356392
/**
@@ -370,6 +406,7 @@ export function buildDeployedState(opts: BuildDeployedStateOptions): DeployedSta
370406
onlineEvalConfigs,
371407
policyEngines,
372408
policies,
409+
runtimeEndpoints,
373410
} = opts;
374411
const targetState: TargetDeployedState = {
375412
resources: {
@@ -404,6 +441,11 @@ export function buildDeployedState(opts: BuildDeployedStateOptions): DeployedSta
404441
targetState.resources!.onlineEvalConfigs = onlineEvalConfigs;
405442
}
406443

444+
// Add runtime endpoint state if endpoints exist
445+
if (runtimeEndpoints && Object.keys(runtimeEndpoints).length > 0) {
446+
targetState.resources!.runtimeEndpoints = runtimeEndpoints;
447+
}
448+
407449
return {
408450
targets: {
409451
...existingState?.targets,

src/cli/commands/deploy/actions.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
parseOnlineEvalOutputs,
1414
parsePolicyEngineOutputs,
1515
parsePolicyOutputs,
16+
parseRuntimeEndpointOutputs,
1617
} from '../../cloudformation';
1718
import { getErrorMessage } from '../../errors';
1819
import { ExecLogger } from '../../logging';
@@ -403,6 +404,17 @@ export async function handleDeploy(options: ValidatedDeployOptions): Promise<Dep
403404
);
404405
const policies = parsePolicyOutputs(outputs, policySpecs);
405406

407+
// Parse runtime endpoint outputs
408+
const endpointSpecs: { agentName: string; endpointName: string }[] = [];
409+
for (const runtime of context.projectSpec.runtimes) {
410+
if (runtime.endpoints) {
411+
for (const endpointName of Object.keys(runtime.endpoints)) {
412+
endpointSpecs.push({ agentName: runtime.name, endpointName });
413+
}
414+
}
415+
}
416+
const runtimeEndpoints = parseRuntimeEndpointOutputs(outputs, endpointSpecs);
417+
406418
// Parse gateway outputs
407419
const gatewaySpecs =
408420
mcpSpec?.agentCoreGateways?.reduce(
@@ -428,6 +440,7 @@ export async function handleDeploy(options: ValidatedDeployOptions): Promise<Dep
428440
onlineEvalConfigs,
429441
policyEngines,
430442
policies,
443+
runtimeEndpoints,
431444
});
432445
await configIO.writeDeployedState(deployedState);
433446

src/cli/commands/remove/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ export type ResourceType =
22
| 'agent'
33
| 'gateway'
44
| 'gateway-target'
5+
| 'runtime-endpoint'
56
| 'memory'
67
| 'credential'
78
| 'evaluator'

src/cli/commands/status/action.ts

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,13 @@ export interface ResourceStatusEntry {
1818
| 'evaluator'
1919
| 'online-eval'
2020
| 'policy-engine'
21-
| 'policy';
21+
| 'policy'
22+
| 'runtime-endpoint';
2223
name: string;
2324
deploymentState: ResourceDeploymentState;
2425
identifier?: string;
2526
detail?: string;
27+
parentName?: string;
2628
error?: string;
2729
invocationUrl?: string;
2830
}
@@ -79,13 +81,15 @@ function diffResourceSet<TLocal extends { name: string }, TDeployed>({
7981
getIdentifier,
8082
getLocalDetail,
8183
getDeployedKey,
84+
getParentName,
8285
}: {
8386
resourceType: ResourceStatusEntry['resourceType'];
8487
localItems: TLocal[];
8588
deployedRecord: Record<string, TDeployed>;
8689
getIdentifier: (deployed: TDeployed) => string | undefined;
8790
getLocalDetail?: (item: TLocal) => string | undefined;
8891
getDeployedKey?: (item: TLocal) => string;
92+
getParentName?: (item: TLocal) => string | undefined;
8993
}): ResourceStatusEntry[] {
9094
const entries: ResourceStatusEntry[] = [];
9195
const localKeys = new Set(localItems.map(item => (getDeployedKey ? getDeployedKey(item) : item.name)));
@@ -99,16 +103,20 @@ function diffResourceSet<TLocal extends { name: string }, TDeployed>({
99103
deploymentState: deployed ? 'deployed' : 'local-only',
100104
identifier: deployed ? getIdentifier(deployed) : undefined,
101105
detail: getLocalDetail?.(item),
106+
parentName: getParentName?.(item),
102107
});
103108
}
104109

105110
for (const [name, deployed] of Object.entries(deployedRecord)) {
106111
if (!localKeys.has(name)) {
112+
// For pending-removal entries, try to extract parentName from composite key
113+
const slashIdx = name.indexOf('/');
107114
entries.push({
108115
resourceType,
109116
name,
110117
deploymentState: 'pending-removal',
111118
identifier: getIdentifier(deployed),
119+
parentName: getParentName && slashIdx > 0 ? name.substring(0, slashIdx) : undefined,
112120
});
113121
}
114122
}
@@ -202,8 +210,34 @@ export function computeResourceStatuses(
202210
getDeployedKey: item => `${item.engineName}/${item.name}`,
203211
});
204212

213+
// Flatten runtime endpoints for diffing against deployed state
214+
const localEndpoints: { name: string; agentName: string; version: number; description?: string }[] = [];
215+
for (const runtime of project.runtimes) {
216+
if (runtime.endpoints) {
217+
for (const [epName, ep] of Object.entries(runtime.endpoints)) {
218+
localEndpoints.push({
219+
name: epName,
220+
agentName: runtime.name,
221+
version: ep.version,
222+
description: ep.description,
223+
});
224+
}
225+
}
226+
}
227+
228+
const runtimeEndpoints = diffResourceSet({
229+
resourceType: 'runtime-endpoint',
230+
localItems: localEndpoints,
231+
deployedRecord: resources?.runtimeEndpoints ?? {},
232+
getIdentifier: deployed => deployed.endpointArn,
233+
getLocalDetail: item => `v${item.version}${item.description ? ` — ${item.description}` : ''}`,
234+
getDeployedKey: item => `${item.agentName}/${item.name}`,
235+
getParentName: item => item.agentName,
236+
});
237+
205238
return [
206239
...agents,
240+
...runtimeEndpoints,
207241
...credentials,
208242
...memories,
209243
...gateways,

src/cli/commands/status/command.tsx

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { Box, Text, render } from 'ink';
99

1010
const VALID_RESOURCE_TYPES = [
1111
'agent',
12+
'runtime-endpoint',
1213
'memory',
1314
'credential',
1415
'gateway',
@@ -58,7 +59,7 @@ export const registerStatus = (program: Command) => {
5859
.option('--target <name>', 'Select deployment target')
5960
.option(
6061
'--type <type>',
61-
'Filter by resource type (agent, memory, credential, gateway, evaluator, online-eval, policy-engine, policy)'
62+
'Filter by resource type (agent, runtime-endpoint, memory, credential, gateway, evaluator, online-eval, policy-engine, policy)'
6263
)
6364
.option('--state <state>', 'Filter by deployment state (deployed, local-only, pending-removal)')
6465
.option('--runtime <name>', 'Filter to a specific runtime')
@@ -135,6 +136,7 @@ export const registerStatus = (program: Command) => {
135136

136137
const filtered = filterResources(result.resources, cliOptions);
137138
const agents = filtered.filter(r => r.resourceType === 'agent');
139+
const runtimeEndpoints = filtered.filter(r => r.resourceType === 'runtime-endpoint');
138140
const credentials = filtered.filter(r => r.resourceType === 'credential');
139141
const memories = filtered.filter(r => r.resourceType === 'memory');
140142
const gateways = filtered.filter(r => r.resourceType === 'gateway');
@@ -153,15 +155,41 @@ export const registerStatus = (program: Command) => {
153155
{agents.length > 0 && (
154156
<Box flexDirection="column" marginTop={1}>
155157
<Text bold>Agents</Text>
156-
{agents.map(entry => (
157-
<Box key={`${entry.resourceType}-${entry.name}`} flexDirection="column">
158-
<ResourceEntry entry={entry} showRuntime />
159-
{entry.invocationUrl && (
160-
<Text dimColor>
161-
{' '}URL: {entry.invocationUrl}
162-
</Text>
163-
)}
164-
</Box>
158+
{agents.map(entry => {
159+
// Find endpoints belonging to this agent
160+
const agentEndpoints = runtimeEndpoints.filter(ep => ep.parentName === entry.name);
161+
return (
162+
<Box key={`${entry.resourceType}-${entry.name}`} flexDirection="column">
163+
<ResourceEntry entry={entry} showRuntime />
164+
{entry.invocationUrl && (
165+
<Text dimColor>
166+
{' '}URL: {entry.invocationUrl}
167+
</Text>
168+
)}
169+
{agentEndpoints.map(ep => (
170+
<Text key={`${ep.parentName}/${ep.name}`}>
171+
{' '}{ep.name} <Text dimColor>{ep.detail}</Text>{' '}
172+
<Text color={DEPLOYMENT_STATE_COLORS[ep.deploymentState] ?? 'gray'}>
173+
[{DEPLOYMENT_STATE_LABELS[ep.deploymentState] ?? ep.deploymentState}]
174+
</Text>
175+
</Text>
176+
))}
177+
</Box>
178+
);
179+
})}
180+
</Box>
181+
)}
182+
183+
{agents.length === 0 && runtimeEndpoints.length > 0 && (
184+
<Box flexDirection="column" marginTop={1}>
185+
<Text bold>Runtime Endpoints</Text>
186+
{runtimeEndpoints.map(ep => (
187+
<Text key={`${ep.parentName}/${ep.name}`}>
188+
{' '}{ep.parentName}/{ep.name} <Text dimColor>{ep.detail}</Text>{' '}
189+
<Text color={DEPLOYMENT_STATE_COLORS[ep.deploymentState] ?? 'gray'}>
190+
[{DEPLOYMENT_STATE_LABELS[ep.deploymentState] ?? ep.deploymentState}]
191+
</Text>
192+
</Text>
165193
))}
166194
</Box>
167195
)}

src/cli/logging/remove-logger.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export interface RemoveLoggerOptions {
1313
| 'credential'
1414
| 'gateway'
1515
| 'gateway-target'
16+
| 'runtime-endpoint'
1617
| 'evaluator'
1718
| 'online-eval'
1819
| 'policy-engine'

0 commit comments

Comments
 (0)