Skip to content

Commit 6241e3b

Browse files
committed
feat: add GovCloud multi-partition support
Add partition-aware ARN construction, endpoint URL generation, and console URL generation to support aws-us-gov (and future aws-cn) partitions. - Create src/cli/aws/partition.ts with getPartition, arnPrefix, dnsSuffix, serviceEndpoint, and consoleDomain utilities - Replace all hardcoded arn:aws: in ARN template literals with arnPrefix(region) - Update ARN regex patterns to accept any partition (arn:[^:]+:) - Replace hardcoded amazonaws.com in endpoint URLs with serviceEndpoint() - Replace hardcoded console.aws.amazon.com with consoleDomain() - Add us-gov-west-1 to AgentCoreRegionSchema, BEDROCK_REGIONS, and LLM compacted types - Add aws-us-gov to cdk.json target-partitions - Fix execution-role-policy.json to use partition wildcard (arn:*) - Add 15 unit tests for partition utilities - Document multi-partition rules and checklists in AGENTS.md
1 parent 13f16d3 commit 6241e3b

22 files changed

Lines changed: 203 additions & 26 deletions

File tree

AGENTS.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,44 @@ See `docs/TESTING.md` for details.
143143
- Always look for existing types before creating a new type inline.
144144
- Re-usable constants must be defined in a constants file in the closest sensible subdirectory.
145145

146+
## Multi-Partition Support (GovCloud, China)
147+
148+
The CLI supports multiple AWS partitions (commercial, GovCloud, China) through a central utility at
149+
`src/cli/aws/partition.ts`. This module maps region prefixes to partition-specific values.
150+
151+
### Rules
152+
153+
- **Never hardcode `arn:aws:`** in ARN construction. Use `arnPrefix(region)` from `src/cli/aws/partition.ts`.
154+
- **Never hardcode `amazonaws.com`** in endpoint URLs. Use `serviceEndpoint(service, region)` or `dnsSuffix(region)`.
155+
- **Never hardcode `console.aws.amazon.com`** in console URLs. Use `consoleDomain(region)`.
156+
- **ARN regex patterns** must use `arn:[^:]+:` (not `arn:aws:`) to match any partition.
157+
- **Static JSON assets** (e.g., IAM policies in `src/assets/`) cannot use TypeScript utilities — use `arn:*:` as the
158+
partition wildcard since IAM evaluates it across all partitions.
159+
160+
### Adding a New Region
161+
162+
Update these files in the CLI repo:
163+
164+
1. `src/schema/schemas/aws-targets.ts` — add to `AgentCoreRegionSchema` enum
165+
2. `src/schema/llm-compacted/aws-targets.ts` — add to `AgentCoreRegion` type union
166+
3. `src/schema/schemas/__tests__/aws-targets.test.ts` — add to `validRegions` array
167+
4. `src/cli/operations/agent/import/constants.ts` — add to `BEDROCK_REGIONS` (if applicable to Bedrock Agent import)
168+
169+
Update these files in the CDK repo (`@aws/agentcore-cdk`):
170+
171+
5. `src/schema/schemas/aws-targets.ts` — add to `AgentCoreRegionSchema` enum
172+
6. `src/schema/llm-compacted/aws-targets.ts` — add to `AgentCoreRegion` type union
173+
174+
Then run `npm run test:update-snapshots` in the CLI repo if any asset files changed.
175+
176+
### Adding a New Partition
177+
178+
1. Add a new entry to `PARTITION_CONFIGS` in `src/cli/aws/partition.ts` with the region prefix, partition name, DNS
179+
suffix, and console domain.
180+
2. Add tests for the new partition in `src/cli/aws/__tests__/partition.test.ts`.
181+
3. Update `src/assets/cdk/cdk.json` — add the partition to `@aws-cdk/core:target-partitions`.
182+
4. Run `npm run test:update-snapshots` to update asset snapshots.
183+
146184
## TUI Harness
147185

148186
See `docs/tui-harness.md` for the full TUI harness usage guide (MCP tools, screen markers, examples, and error

src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ exports[`Assets Directory Snapshots > CDK assets > cdk/cdk/cdk.json should match
149149
"@aws-cdk/aws-ecs-patterns:secGroupsDisablesImplicitOpenListener": true,
150150
"@aws-cdk/aws-lambda:recognizeLayerVersion": true,
151151
"@aws-cdk/core:checkSecretUsage": true,
152-
"@aws-cdk/core:target-partitions": ["aws", "aws-cn"],
152+
"@aws-cdk/core:target-partitions": ["aws", "aws-cn", "aws-us-gov"],
153153
"@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true,
154154
"@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true,
155155
"@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true,

src/assets/cdk/cdk.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
"@aws-cdk/aws-ecs-patterns:secGroupsDisablesImplicitOpenListener": true,
1010
"@aws-cdk/aws-lambda:recognizeLayerVersion": true,
1111
"@aws-cdk/core:checkSecretUsage": true,
12-
"@aws-cdk/core:target-partitions": ["aws", "aws-cn"],
12+
"@aws-cdk/core:target-partitions": ["aws", "aws-cn", "aws-us-gov"],
1313
"@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true,
1414
"@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true,
1515
"@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true,

src/assets/evaluators/python-lambda/execution-role-policy.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
{
55
"Effect": "Allow",
66
"Action": ["logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents"],
7-
"Resource": "arn:aws:logs:*:*:log-group:/aws/lambda/*"
7+
"Resource": "arn:*:logs:*:*:log-group:/aws/lambda/*"
88
}
99
]
1010
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { arnPrefix, consoleDomain, dnsSuffix, getPartition, serviceEndpoint } from '../partition';
2+
import { describe, expect, it } from 'vitest';
3+
4+
describe('getPartition', () => {
5+
it('returns aws for standard commercial regions', () => {
6+
expect(getPartition('us-east-1')).toBe('aws');
7+
expect(getPartition('eu-west-1')).toBe('aws');
8+
expect(getPartition('ap-southeast-1')).toBe('aws');
9+
});
10+
11+
it('returns aws-us-gov for GovCloud regions', () => {
12+
expect(getPartition('us-gov-west-1')).toBe('aws-us-gov');
13+
expect(getPartition('us-gov-east-1')).toBe('aws-us-gov');
14+
});
15+
16+
it('returns aws-cn for China regions', () => {
17+
expect(getPartition('cn-north-1')).toBe('aws-cn');
18+
expect(getPartition('cn-northwest-1')).toBe('aws-cn');
19+
});
20+
});
21+
22+
describe('arnPrefix', () => {
23+
it('returns arn:aws for commercial regions', () => {
24+
expect(arnPrefix('us-east-1')).toBe('arn:aws');
25+
});
26+
27+
it('returns arn:aws-us-gov for GovCloud regions', () => {
28+
expect(arnPrefix('us-gov-west-1')).toBe('arn:aws-us-gov');
29+
});
30+
31+
it('returns arn:aws-cn for China regions', () => {
32+
expect(arnPrefix('cn-north-1')).toBe('arn:aws-cn');
33+
});
34+
});
35+
36+
describe('dnsSuffix', () => {
37+
it('returns amazonaws.com for commercial regions', () => {
38+
expect(dnsSuffix('us-east-1')).toBe('amazonaws.com');
39+
});
40+
41+
it('returns amazonaws.com for GovCloud regions', () => {
42+
expect(dnsSuffix('us-gov-west-1')).toBe('amazonaws.com');
43+
});
44+
45+
it('returns amazonaws.com.cn for China regions', () => {
46+
expect(dnsSuffix('cn-north-1')).toBe('amazonaws.com.cn');
47+
});
48+
});
49+
50+
describe('serviceEndpoint', () => {
51+
it('builds correct endpoint for commercial regions', () => {
52+
expect(serviceEndpoint('bedrock-agentcore', 'us-east-1')).toBe('bedrock-agentcore.us-east-1.amazonaws.com');
53+
});
54+
55+
it('builds correct endpoint for GovCloud regions', () => {
56+
expect(serviceEndpoint('bedrock-agentcore', 'us-gov-west-1')).toBe('bedrock-agentcore.us-gov-west-1.amazonaws.com');
57+
});
58+
59+
it('builds correct endpoint for China regions', () => {
60+
expect(serviceEndpoint('bedrock-agentcore', 'cn-north-1')).toBe('bedrock-agentcore.cn-north-1.amazonaws.com.cn');
61+
});
62+
});
63+
64+
describe('consoleDomain', () => {
65+
it('returns console.aws.amazon.com for commercial regions', () => {
66+
expect(consoleDomain('us-east-1')).toBe('console.aws.amazon.com');
67+
});
68+
69+
it('returns console.amazonaws-us-gov.com for GovCloud regions', () => {
70+
expect(consoleDomain('us-gov-west-1')).toBe('console.amazonaws-us-gov.com');
71+
});
72+
73+
it('returns console.amazonaws.cn for China regions', () => {
74+
expect(consoleDomain('cn-north-1')).toBe('console.amazonaws.cn');
75+
});
76+
});

src/cli/aws/agentcore.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { parseJsonRpcResponse } from '../../lib/utils/json-rpc';
22
import { getCredentialProvider } from './account';
3+
import { serviceEndpoint } from './partition';
34
import {
45
BedrockAgentCoreClient,
56
EvaluateCommand,
@@ -142,11 +143,10 @@ export function extractResult(text: string): string {
142143

143144
/**
144145
* Build the invoke URL for a runtime ARN.
145-
* Format: https://bedrock-agentcore.{REGION}.amazonaws.com/runtimes/{ESCAPED_ARN}/invocations?qualifier=DEFAULT
146146
*/
147147
function buildInvokeUrl(region: string, runtimeArn: string): string {
148148
const escapedArn = encodeURIComponent(runtimeArn);
149-
return `https://bedrock-agentcore.${region}.amazonaws.com/runtimes/${escapedArn}/invocations?qualifier=DEFAULT`;
149+
return `https://${serviceEndpoint('bedrock-agentcore', region)}/runtimes/${escapedArn}/invocations?qualifier=DEFAULT`;
150150
}
151151

152152
/**

src/cli/aws/bedrock-import.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -323,7 +323,7 @@ async function fetchCollaborators(
323323
const aliasArn = (summary as { agentDescriptor?: { aliasArn?: string } }).agentDescriptor?.aliasArn;
324324
if (!aliasArn) continue;
325325

326-
const arnMatch = /^arn:aws:bedrock:[^:]+:[^:]+:agent-alias\/([^/]+)\/([^/]+)$/.exec(aliasArn);
326+
const arnMatch = /^arn:[^:]+:bedrock:[^:]+:[^:]+:agent-alias\/([^/]+)\/([^/]+)$/.exec(aliasArn);
327327
if (!arnMatch) continue;
328328
const [, collabAgentId, collabAliasId] = arnMatch;
329329
if (!collabAgentId || !collabAliasId) continue;

src/cli/aws/cloudwatch.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { getCredentialProvider } from './account';
2+
import { arnPrefix } from './partition';
23
import { CloudWatchLogsClient, FilterLogEventsCommand, StartLiveTailCommand } from '@aws-sdk/client-cloudwatch-logs';
34

45
export interface LogEvent {
@@ -31,7 +32,7 @@ export async function* streamLogs(options: StreamLogsOptions): AsyncGenerator<Lo
3132
const { logGroupName, region, accountId, filterPattern, abortSignal } = options;
3233

3334
// StartLiveTail requires ARN format for logGroupIdentifiers
34-
const logGroupArn = `arn:aws:logs:${region}:${accountId}:log-group:${logGroupName}`;
35+
const logGroupArn = `${arnPrefix(region)}:logs:${region}:${accountId}:log-group:${logGroupName}`;
3536

3637
while (!abortSignal?.aborted) {
3738
const client = new CloudWatchLogsClient({

src/cli/aws/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export { detectAwsContext, type AwsContext } from './aws-context';
22
export { detectAccount, getCredentialProvider } from './account';
3+
export { getPartition, arnPrefix, dnsSuffix, serviceEndpoint, consoleDomain } from './partition';
34
export { detectRegion, type RegionDetectionResult } from './region';
45
export {
56
invokeBedrockSync,

src/cli/aws/partition.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
interface PartitionConfig {
2+
partition: string;
3+
dnsSuffix: string;
4+
consoleDomain: string;
5+
}
6+
7+
const PARTITION_CONFIGS: { prefix: string; config: PartitionConfig }[] = [
8+
{
9+
prefix: 'us-gov-',
10+
config: { partition: 'aws-us-gov', dnsSuffix: 'amazonaws.com', consoleDomain: 'console.amazonaws-us-gov.com' },
11+
},
12+
{
13+
prefix: 'cn-',
14+
config: { partition: 'aws-cn', dnsSuffix: 'amazonaws.com.cn', consoleDomain: 'console.amazonaws.cn' },
15+
},
16+
];
17+
18+
const DEFAULT_CONFIG: PartitionConfig = {
19+
partition: 'aws',
20+
dnsSuffix: 'amazonaws.com',
21+
consoleDomain: 'console.aws.amazon.com',
22+
};
23+
24+
export function getPartition(region: string): string {
25+
return getPartitionConfig(region).partition;
26+
}
27+
28+
export function arnPrefix(region: string): string {
29+
return `arn:${getPartition(region)}`;
30+
}
31+
32+
export function dnsSuffix(region: string): string {
33+
return getPartitionConfig(region).dnsSuffix;
34+
}
35+
36+
export function serviceEndpoint(service: string, region: string): string {
37+
return `${service}.${region}.${dnsSuffix(region)}`;
38+
}
39+
40+
export function consoleDomain(region: string): string {
41+
return getPartitionConfig(region).consoleDomain;
42+
}
43+
44+
function getPartitionConfig(region: string): PartitionConfig {
45+
for (const { prefix, config } of PARTITION_CONFIGS) {
46+
if (region.startsWith(prefix)) return config;
47+
}
48+
return DEFAULT_CONFIG;
49+
}

0 commit comments

Comments
 (0)