feat: add gateway import command with executionRoleArn support#855
feat: add gateway import command with executionRoleArn support#855jesseturner21 wants to merge 20 commits intoaws:mainfrom
Conversation
Add `agentcore import gateway --arn <arn>` to import existing AWS gateways (with all targets) into a local CLI project. Also remove import from the HIDDEN_FROM_TUI list so it appears in the interactive TUI. - Add AWS SDK wrappers for gateway/target list/get APIs - Add import-gateway.ts with multi-resource CFN import support - Add resourceName schema field to preserve actual AWS gateway name during import - Register gateway in TUI ImportSelectScreen and ImportProgressScreen - Extend ARN pattern, deployed state, and CFN constants for gateway type
The ARN text input was truncating long ARNs. Use the expandable prop to wrap text across multiple lines. Also add gateway to the ARN validation pattern and resource type labels.
Remove --name (confusing local rename) and --yes (no prompts to confirm) from the gateway import command. The gateway's AWS name is used directly.
Add end-to-end tests that create a real AWS gateway with an MCP server target, import it via `agentcore import gateway --arn`, and verify the resulting agentcore.json fields and deployed-state.json entries. New files: - e2e-tests/fixtures/import/setup_gateway.py: creates gateway + target - e2e-tests/fixtures/import/common.py: gateway wait helpers - e2e-tests/fixtures/import/cleanup_resources.py: gateway cleanup Constraint: Tests follow the existing import-resources.test.ts pattern Confidence: high Scope-risk: narrow
Extract roleArn from the AWS GetGateway response and map it to executionRoleArn in agentcore.json. On deploy, CDK uses iam.Role.fromRoleArn() instead of creating a new role, keeping the original permissions intact. Constraint: imported roles use mutable: false so CDK cannot modify them Rejected: always create new role | breaks permissions on re-import Confidence: high Scope-risk: narrow
Package TarballHow to installnpm install https://github.com/aws/agentcore-cli/releases/download/pr-855-tarball/aws-agentcore-0.11.0.tgz |
Add @internal exports for toGatewayTargetSpec, resolveOutboundAuth, toGatewaySpec, and buildCredentialArnMap to enable direct unit testing of the pure mapping functions in import-gateway.ts. Confidence: high Scope-risk: narrow
…lution Bugbash coverage for toGatewayTargetSpec and resolveOutboundAuth: - mcpServer with no auth, OAuth, and API_KEY credentials - Credential resolution warnings when ARNs not in project - Targets with no MCP configuration - OAuth scopes pass-through and empty scopes omission 8 tests, all passing. Confidence: high Scope-risk: narrow
…da target mapping Bugbash coverage for toGatewayTargetSpec non-mcpServer target types: - apiGateway: restApiId, stage, toolFilters, toolOverrides mapping - openApiSchema: S3 URI mapping, missing URI warning - smithyModel: S3 URI mapping, missing URI warning - lambda: S3 tool schema to lambdaFunctionArn mapping, missing ARN, inline-only schema warning, progress messages - Unrecognized target type warning 13 tests, all passing. Confidence: high Scope-risk: narrow
Bugbash coverage for toGatewaySpec AWS-to-CLI schema mapping: - Authorizer types: NONE, AWS_IAM, CUSTOM_JWT with all JWT fields - CUSTOM_JWT customClaims with full claim structure - Semantic search: SEMANTIC/KEYWORD/missing protocolConfiguration - Exception level: DEBUG/undefined/other values - Policy engine: ARN name extraction, mode preservation - Optional fields: resourceName, description, tags, executionRoleArn - Edge cases: empty tags object omitted, empty JWT arrays omitted 23 tests, all passing. Confidence: high Scope-risk: narrow
Bugbash coverage for the main gateway import flow: - Happy path: successful import with --arn, config written, result verified - Rollback: pipeline failure restores original config, noResources error - Duplicate detection: name collision, resource ID already tracked - Name validation: invalid name regex, --name override preserves resourceName - Auto-select: single gateway auto-selected, multiple gateways error, no gateways error - Target mapping: skipped targets warning, non-READY gateway continues 12 tests, all passing. Confidence: high Scope-risk: narrow
Bugbash coverage for credential resolution and CFN resource matching: - buildCredentialArnMap: reads ARN-to-name map from deployed state, handles multiple credentials, empty/missing state, thrown errors - findLogicalIdByProperty: gateway by Name property, resourceName fallback, target by Name, Fn::Join/Fn::Sub intrinsic function patterns, regex boundary check prevents false substring matches - findLogicalIdsByType: single gateway fallback, single target fallback, multiple targets prevent fallback 14 tests, all passing. Confidence: high Scope-risk: narrow
…ce list When a project already contains an imported resource (gateway + target, agent, memory, etc.), a subsequent import of a different resource that shares a Name with the deployed one caused buildResourcesToImport to resolve the OLD logical ID via findLogicalIdByProperty. The resulting CFN change set then failed with "Resources [...] passed in ResourceToImport are already in a stack and cannot be imported." Thread the deployed template into every buildResourcesToImport callback and skip logical IDs already present in the stack during both the name lookup and the single-candidate fallback. Constraint: GatewayTarget has no structural parent ref in Properties — only the physical-ID tuple (GatewayIdentifier, TargetId), so scoping the synth search by parent gateway is not available. Rejected: Parse Fn::Ref/Fn::GetAtt from GatewayIdentifier | brittle intrinsic traversal Rejected: Match by physical TargetId | synth template has no physical ID for new resources Rejected: Strip deployed resources from synth before lookup | breaks buildImportTemplate Confidence: high Scope-risk: narrow Directive: new callbacks into executeCdkImportPipeline must accept and honor the deployedTemplate arg Not-tested: multi-region / cross-stack-identifier collisions
…ound error When importing a gateway by a well-formed but nonexistent ARN, the BedrockAgentCore control plane returns AccessDenied (not ResourceNotFound) for bedrock-agentcore:GetGateway. The CLI surfaced the raw SDK error — which is misleading when the caller has full Admin access and the gateway simply doesn't exist. Catch AccessDenied from getGatewayDetail and return a targeted failure with guidance: the gateway is likely nonexistent / the ARN is malformed / the caller lacks GetGateway. Point the user at list-gateways so they can confirm. Constraint: AWS returns AccessDenied instead of ResourceNotFound for nonexistent gateway IDs; we cannot distinguish the two server-side Rejected: Client-side ARN existence probe via ListGateways | extra latency on the happy path and still racy Confidence: high Scope-risk: narrow Directive: Do not swallow other error classes here — only AccessDenied is reinterpreted
Previously when a user ran import with AWS_REGION=us-west-2 against a us-east-1 ARN, and no deployment targets existed yet, the CLI silently synthesized a default target from the ARN's region and proceeded — so the user would unknowingly import from a different region than they intended, leaving agentcore.json pointed at the wrong region and causing cross-region CFN errors on later deploy. Short-circuit resolveImportTarget when AWS_REGION (or AWS_DEFAULT_REGION) is set and disagrees with the ARN's region, and ask the user to reconcile explicitly. Constraint: Must fail fast before any side effects (writing aws-targets.json, calling GetGateway) Rejected: Warn-and-continue | a silent cross-region import is exactly the failure mode we're preventing Confidence: high Scope-risk: narrow Directive: Only throw when both env region AND ARN region are present — do not require AWS_REGION to be set
…eline After `agentcore remove gateway`, the gateway entry remains in deployed-state.json (correctly, since CFN still manages it) but is removed from agentcore.json. A subsequent `agentcore import gateway` would reject with "already imported" because the dedup check only looked at deployed-state. Now, when a resource exists in deployed-state but not in agentcore.json, the import skips the CDK pipeline and just re-adds the resource to agentcore.json. Applies to both the gateway-specific and generic import orchestrators.
aidandaly24
left a comment
There was a problem hiding this comment.
Comprehensive review
Reviewed via 5 parallel Opus 4.7 agents tracing the CLI command flow, CDK Gateway construct, CFN IMPORT pipeline, cross-repo schema sync, and AWS→CLI mapping end-to-end. All findings below were verified against the current PR head (343469b) by a second pass.
Blockers that need a fix before merge (inline on each):
- B3:
import gatewaymissing--name/--target/-yCommander flags - B4: unresolvable OAuth/API_KEY credential silently strips
outboundAuth→ next deploy removes live auth - B6:
enableSemanticSearch: false→ CDK omits ProtocolConfiguration → drift on first UPDATE after IMPORT - B7:
lambda→lambdaFunctionArnswap erases OAuth on live Lambda targets
High-priority (inline where possible):
- H1: gateway logical-ID fallback chain misses when user passes
--name+ ≥2 un-deployed gateways - H2: re-import fast path writes out-of-band targets to config without reconciling with CFN
- H3: re-import with
--nameoverride leaves deployed-state keyed under the old name - H5:
parseAndValidateArnpre-flight regexes hardcodearn:aws:— blocks GovCloud/China (AGENTS.md violation) - H7:
executionRoleArn/resourceNamearez.string().optional()with no validation - H8:
resourceNameandexecutionRoleArndon't co-vary in the schema
Two findings whose target lines fall outside the PR diff (can't be inlined — flagging here):
H9 — LLM-compacted schemas missing the new fields. src/schema/llm-compacted/mcp.ts (file is not in this diff): the AgentCoreGateway interface there omits resourceName and executionRoleArn. These compacted files are vended as read-only LLM context via cli/templates/schema-assets.ts, so LLMs generating user agentcore.json files won't know these fields exist. Same gap in the companion CDK PR. Fix: add both fields to both files.
H10 — Live openApiSchema target with GATEWAY_IAM_ROLE is uninportable. src/schema/schemas/mcp.ts:57 (outside the hunk touched by this PR): TARGET_TYPE_AUTH_CONFIG.openApiSchema has authRequired: true and iamRoleFallback: false. But resolveOutboundAuth at import-gateway.ts:214-217 falls through to return undefined for GATEWAY_IAM_ROLE, and AWS service-side accepts GATEWAY_IAM_ROLE on OpenAPI targets. So a legitimate live config maps to a spec with no outboundAuth and fails Zod superRefine. Fix: change to { authRequired: false, validAuthTypes: ['OAUTH', 'API_KEY'], iamRoleFallback: true } and update the superRefine message.
Nice work overall — the CFN IMPORT pipeline reuse and rollback logic are clean. Pre-existing cross-repo schema drift (artifact vs build, AgentCoreMcpSpecSchema missing from CLI) is out of scope for this PR but worth a separate tracking issue.
| importCmd | ||
| .command('gateway') | ||
| .description('Import an existing AgentCore Gateway (with targets) from your AWS account') | ||
| .option('--arn <gatewayArn>', 'Gateway ARN to import') |
There was a problem hiding this comment.
B3 (blocker) — Missing Commander flags.
Only --arn is registered here, but handleImportGateway reads options.name, options.target, options.yes (lines 428, 452, and via ImportResourceOptions). Commander's default behavior rejects unknown options with process.exit(1) unless allowUnknownOption() is called; grep shows no such call in src/cli. Running agentcore import gateway --name foo will fail at parse time.
The flow test at __tests__/import-gateway-flow.test.ts invokes handleImportGateway({...}) directly and bypasses Commander, so tests cannot catch this.
Sibling import-memory.ts:108-113 and import-runtime.ts:204-212 register all three.
Fix:
importCmd
.command('gateway')
.description('Import an existing AgentCore Gateway (with targets) from your AWS account')
.option('--arn <gatewayArn>', 'Gateway ARN to import')
.option('--name <name>', 'Local name for the imported gateway')
.option('--target <target>', 'Deployment target name (only needed if project has multiple targets)')
.option('-y, --yes', 'Auto-confirm prompts')
.action(/* ... */);| `Warning: Target "${detail.name}" uses OAuth credential (${providerArn}) not found in project. ` + | ||
| 'Configure credentials manually after import with `agentcore add credential`.' | ||
| ); | ||
| return undefined; |
There was a problem hiding this comment.
B4 (blocker) — Unresolvable credential silently strips outboundAuth; next deploy removes live auth.
When credentials.get(providerArn) misses, we return undefined and the caller spreads ...(outboundAuth && { outboundAuth }), producing a target spec with no outboundAuth key. Per-target consequences:
mcpServer/apiGateway: spec validates (default NONE). CDK then emitscredentialProviderConfigurations: undefined. CloudFormation UPDATE with the property removed reverts it to service default — live credential provider is stripped.openApiSchema: fails ZodsuperRefinebecauseauthRequired: true. Import hard-fails atwriteProjectSpecwith a cryptic Zod error (overlaps H10).smithyModel/lambdaFunctionArn: iamRoleFallback, not affected.
Fix: treat "auth configured but unresolvable" as a hard failure distinct from "no auth configured".
if (!credentialName) {
throw new Error(
`Target "${detail.name}" uses OAuth credential provider ${providerArn}, but no matching ` +
`credential exists in this project's deployed-state.json. Import the credential first ` +
`(\`agentcore add credential\` or from the owning project) and re-run.`
);
}Apply the same to the API_KEY branch at line 211.
| }; | ||
| } | ||
|
|
||
| const enableSemanticSearch = gateway.protocolConfiguration?.mcp?.searchType === 'SEMANTIC'; |
There was a problem hiding this comment.
B6 (blocker) — enableSemanticSearch: false produces ProtocolConfiguration drift.
const enableSemanticSearch = gateway.protocolConfiguration?.mcp?.searchType === 'SEMANTIC';Three live states collapse to two import values: SEMANTIC → true; explicit NONE or absent → false. The CDK companion (Gateway.ts:223-233 in the companion PR) returns undefined when enableSemanticSearch === false, omitting ProtocolConfiguration from the synth template. CFN IMPORT itself only compares identifier properties, so the import succeeds — but on the first post-import agentcore deploy, CFN sees ProtocolConfiguration absent in template vs present-on-live and issues an UpdateGateway call rewriting live state.
Fix: emit explicit NONE/SEMANTIC in both CDK and mapping. In the companion CDK PR:
private buildProtocolConfiguration(gateway: AgentCoreGateway) {
return {
mcp: { searchType: gateway.enableSemanticSearch === false ? 'NONE' : 'SEMANTIC' },
};
}| lambdaFunctionArn: { | ||
| lambdaArn, | ||
| toolSchemaFile: s3Uri, | ||
| }, |
There was a problem hiding this comment.
B7 (blocker) — Live Lambda target with OAuth outbound auth silently loses OAuth on import.
TARGET_TYPE_AUTH_CONFIG.lambda at schemas/mcp.ts:61 allows ['OAUTH', 'NONE'], so a live AWS Lambda-backed target CAN have OAuth. This mapper drops outboundAuth entirely (even the ...(outboundAuth && ...) spread is missing), and the destination lambdaFunctionArn schema at schemas/mcp.ts:62 has validAuthTypes: [] so it couldn't accept OAuth anyway. On top of that, the CDK companion at Gateway.ts:398 hardcodes [{ credentialProviderType: 'GATEWAY_IAM_ROLE' }], overwriting the live OAuth provider on next deploy.
Fix: preserve outboundAuth in the mapper, relax TARGET_TYPE_AUTH_CONFIG.lambdaFunctionArn.validAuthTypes, and wire buildCredentialConfig(target) into the CDK lambdaFunctionArn branch.
if (s3Uri) {
onProgress(`Mapping compute-backed Lambda target "${detail.name}" to lambdaFunctionArn type`);
return {
name: detail.name,
targetType: 'lambdaFunctionArn',
lambdaFunctionArn: { lambdaArn, toolSchemaFile: s3Uri },
...(outboundAuth && { outboundAuth }),
};
}| localName, | ||
| { excludeLogicalIds: deployedIds } | ||
| ); | ||
| if (!gatewayLogicalId) { |
There was a problem hiding this comment.
H1 (high) — Logical-ID fallback chain misses with --name + multiple un-deployed gateways.
Tiers 1-2 search for ${projectName}-${localName} and localName, but the CDK synthesizes Name: gateway.resourceName (the live AWS name, set at line 282). When the user passes --name foo that differs from the AWS name, localName='foo' and resourceName=awsName — neither tier matches. Tier 3 (single-candidate) saves only when exactly one un-deployed gateway logical ID exists. If the project has ≥2 new gateways in the synth, all tiers miss → return [] → rollback.
Currently latent because --name isn't wired through Commander (see B3), but will be reachable once B3 is fixed.
Fix: add gatewayDetail.name as an explicit fallback.
gatewayLogicalId ??= findLogicalIdByProperty(
synthTemplate,
'AWS::BedrockAgentCore::Gateway',
'Name',
gatewayDetail.name,
{ excludeLogicalIds: deployedIds }
);| logger.endStep('success'); | ||
|
|
||
| logger.finalize(true); | ||
| return { |
There was a problem hiding this comment.
H2 (high) — Re-import fast path can write out-of-band targets to config.
The fast path fetches all live targets, maps them into mappedTargets, writes to agentcore.json, and returns without touching CFN. If a target was added to the live gateway out-of-band between imports, it gets written into config but not into the CFN stack. The next agentcore deploy synthesizes a CfnGatewayTarget with a Name that matches a live un-managed resource → CloudFormation CREATE fails with CREATE_FAILED (AWS API returns ConflictException/ValidationException).
Fix: run the full pipeline on re-import. The existing buildResourcesToImport callback already handles deployedIds exclusion and produces an empty list when every target is already in the stack, which we can treat as success. Remove the early return here and fall through to the main path.
| const existingResource = await findResourceInDeployedState(ctx.configIO, targetName, 'gateway', gatewayId); | ||
| if (existingResource) { | ||
| if (!options.name) { | ||
| localName = existingResource; |
There was a problem hiding this comment.
H3 (high) — Re-import with --name override leaves deployed-state keyed under old name.
if (!options.name) {
localName = existingResource;
}If the user passes --name newName, localName stays as the user's value. The pushed spec has name: newName but deployed-state.json still has its entry keyed under existingResource. Downstream consumers — status/action.ts, deploy, remove — all match by agentCoreGateways[i].name, so status reports an orphan, remove newName can't clean up deployed-state, and deploy treats it as a new gateway.
Currently latent because --name isn't wired through Commander (see B3).
Fix: reject the mismatch.
if (options.name && options.name !== existingResource) {
return failResult(
logger,
`Gateway "${existingResource}" is already managed under that name in deployed-state.json. ` +
`Either omit --name (the existing name will be reused) or first rename the deployed-state entry.`,
'gateway',
options.name
);
}| if ( | ||
| arn && | ||
| !/^arn:[^:]+:bedrock-agentcore:([^:]+):([^:]+):(runtime|memory|evaluator|online-evaluation-config)\/(.+)$/.test(arn) | ||
| !/^arn:aws:bedrock-agentcore:([^:]+):([^:]+):(runtime|memory|evaluator|online-evaluation-config|gateway)\/(.+)$/.test( |
There was a problem hiding this comment.
H5 (high) — Partition hardcode blocks GovCloud / China.
This regex and the one at line 145 both hardcode arn:aws:. The ARN_PATTERN at line 229 correctly uses arn:[^:]+:, but these pre-flight checks run first and reject GovCloud (arn:aws-us-gov:) and China (arn:aws-cn:) ARNs before the right validator ever runs. Direct violation of AGENTS.md: "ARN regex patterns must use arn:[^:]+:".
Fix:
// line 131-139
if (arn && !ARN_PATTERN.test(arn)) {
throw new Error(
`Not a valid ARN: "${arn}".\nExpected format: arn:<partition>:bedrock-agentcore:<region>:<account>:<runtime|memory|evaluator|online-evaluation-config|gateway>/<id>`
);
}
// line 145
const arnRegionMatch = /^arn:[^:]+:bedrock-agentcore:([^:]+):/.exec(arn);| /** Policy engine configuration for Cedar-based authorization of tool calls. */ | ||
| policyEngineConfiguration: GatewayPolicyEngineConfigurationSchema.optional(), | ||
| /** ARN of an existing IAM execution role. When set, CDK uses this role instead of creating a new one. */ | ||
| executionRoleArn: z.string().optional(), |
There was a problem hiding this comment.
H7 (high) — executionRoleArn / resourceName have no validation.
Both fields are z.string().optional() with no regex, no length bound. Weaker than LambdaFunctionArnConfigSchema.lambdaArn (.min(1).max(170) at line 106) and weaker than the existing GatewayNameSchema. A user hand-writing agentcore.json with a malformed role ARN or invalid gateway name only discovers it at CFN deploy time with a ValidationException.
Fix:
resourceName: GatewayNameSchema.optional(),
executionRoleArn: z.string()
.regex(/^arn:[^:]+:iam::\d{12}:role\/.+/, 'Must be a valid IAM role ARN')
.max(2048)
.optional(),H8 (high, same file) — resourceName and executionRoleArn don't co-vary.
Only-resourceName → CDK creates a fresh role against a custom gateway name (ambiguous config; unclear whether import or create-with-custom-name). Only-executionRoleArn → CDK uses imported role with a project-generated gateway name (the imported role silently drops all addToPolicy calls for outbound-auth targets). Add a .refine():
.refine(
data => (data.resourceName !== undefined) === (data.executionRoleArn !== undefined),
{
message: 'resourceName and executionRoleArn must be set together (import) or both omitted (fresh gateway)',
path: ['resourceName'],
}
)Apply identically to the CDK repo (agentcore-l3-cdk-constructs/src/schema/schemas/mcp.ts:598).
- B4: Hard-fail when credential provider ARN is not found in deployed state instead of silently dropping outboundAuth - B7: Preserve outboundAuth on lambda→lambdaFunctionArn mapping and allow OAUTH/NONE auth types for lambdaFunctionArn targets - H2: Remove re-import fast path; run full CDK pipeline so out-of-band targets are properly imported. Treat noResources as success for re-imports since all resources are already in the CFN stack - H5: Replace hardcoded arn:aws: with partition-agnostic arn:[^:]+: in ARN validation and region extraction regexes - H7/H8: Add regex validation and max length for executionRoleArn, use GatewayNameSchema for resourceName, add refine ensuring both fields are set together or both omitted
| console.log(` agentcore fetch access ${ANSI.dim}Get gateway URL and token${ANSI.reset}`); | ||
| console.log(''); | ||
| } else { | ||
| console.error(`\n${ANSI.red}[error]${ANSI.reset} ${result.error}`); |
Summary
agentcore import gateway --arn <gatewayArn>command that imports an existing AWS gateway with its targets into a local agentcore projectexecutionRoleArn, matching the pattern used by runtime and memory importsiam.Role.fromRoleArn()instead of creating a new role, keeping original permissions intactimportcommand from the TUI and add gateway ARN support to the ARN input componentChanges
Gateway Import Command
src/cli/commands/import/import-gateway.ts— new import command that fetches gateway details + targets from AWS, maps them to the local schema, writes agentcore.json and deployed-state.json, then runs CDK synth for CloudFormation importsrc/cli/aws/agentcore-control.ts— extractroleArnfrom GetGateway API responseexecutionRoleArn Support
src/schema/schemas/mcp.ts— add optionalexecutionRoleArnfield to gateway schemasrc/cli/commands/import/import-gateway.ts— map AWSroleArn→executionRoleArnduring importCDK Constructs (separate PR in agentcore-l3-cdk-constructs)
Gateway.ts— usefromRoleArnwhenexecutionRoleArnis set, addaddToPolicyguard methodAgentCoreMcp.ts— usegateway.addToPolicy()for policy engine grantsmcp.ts— addexecutionRoleArnto CDK schemaE2E Tests
e2e-tests/import-resources.test.ts— gateway import test, field verification (includingexecutionRoleArn), deployed-state verificatione2e-tests/fixtures/import/setup_gateway.py— creates gateway + MCP server target for testinge2e-tests/fixtures/import/common.py— gateway wait helpersTest plan
executionRoleArnmatches original role ARNagentcore.jsoncontains all gateway fields (name, resourceName, description, authorizerType, enableSemanticSearch, exceptionLevel, executionRoleArn, tags, targets)CDK_TARBALL=<path> npm run test:e2e