Skip to content

Commit 098b104

Browse files
authored
feat: add GovCloud multi-partition support (aws#908)
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 ce683c0 commit 098b104

23 files changed

Lines changed: 244 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

eslint.config.mjs

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,63 @@ import reactRefresh from 'eslint-plugin-react-refresh';
77
import security from 'eslint-plugin-security';
88
import tseslint from 'typescript-eslint';
99

10+
/** @type {import('eslint').ESLint.Plugin} */
11+
const partitionPlugin = {
12+
rules: {
13+
'no-hardcoded-arn-partition': {
14+
meta: {
15+
type: 'problem',
16+
docs: { description: 'Disallow hardcoded arn:aws: partition in ARN construction. Use arnPrefix(region) instead.' },
17+
schema: [],
18+
},
19+
create(context) {
20+
function checkForHardcodedArn(node, value) {
21+
if (/arn:aws:/.test(value)) {
22+
context.report({ node, message: 'Hardcoded "arn:aws:" detected. Use arnPrefix(region) from src/cli/aws/partition.ts for multi-partition support.' });
23+
}
24+
}
25+
return {
26+
TemplateLiteral(node) {
27+
for (const quasi of node.quasis) {
28+
checkForHardcodedArn(node, quasi.value.raw);
29+
}
30+
},
31+
};
32+
},
33+
},
34+
'no-hardcoded-endpoint-tld': {
35+
meta: {
36+
type: 'problem',
37+
docs: { description: 'Disallow hardcoded amazonaws.com in endpoint URL construction. Use serviceEndpoint() or dnsSuffix() instead.' },
38+
schema: [],
39+
},
40+
create(context) {
41+
const REGION_PATTERN = /[a-z]{2}(-[a-z]+-\d+)/;
42+
function hasHardcodedEndpoint(value) {
43+
return /\.amazonaws\.com/.test(value);
44+
}
45+
function hasHardcodedEndpointWithRegion(value) {
46+
return hasHardcodedEndpoint(value) && REGION_PATTERN.test(value);
47+
}
48+
return {
49+
TemplateLiteral(node) {
50+
for (const quasi of node.quasis) {
51+
if (hasHardcodedEndpoint(quasi.value.raw)) {
52+
context.report({ node, message: 'Hardcoded ".amazonaws.com" in template literal. Use serviceEndpoint() or dnsSuffix() from src/cli/aws/partition.ts for multi-partition support.' });
53+
}
54+
}
55+
},
56+
Literal(node) {
57+
if (typeof node.value === 'string' && hasHardcodedEndpointWithRegion(node.value)) {
58+
context.report({ node, message: 'Hardcoded endpoint with region detected. Use serviceEndpoint() or dnsSuffix() from src/cli/aws/partition.ts for multi-partition support.' });
59+
}
60+
},
61+
};
62+
},
63+
},
64+
},
65+
};
66+
1067
export default tseslint.config(
1168
eslint.configs.recommended,
1269
...tseslint.configs.recommendedTypeChecked,
@@ -30,8 +87,11 @@ export default tseslint.config(
3087
'react-hooks': reactHooks,
3188
'react-refresh': reactRefresh,
3289
security,
90+
partition: partitionPlugin,
3391
},
3492
rules: {
93+
'partition/no-hardcoded-arn-partition': 'error',
94+
'partition/no-hardcoded-endpoint-tld': 'error',
3595
...importPlugin.configs.recommended.rules,
3696
...react.configs.recommended.rules,
3797
...security.configs.recommended.rules,
@@ -68,6 +128,8 @@ export default tseslint.config(
68128
{
69129
files: ['**/*.test.ts', '**/*.test.tsx', '**/test-utils/**', 'integ-tests/**'],
70130
rules: {
131+
'partition/no-hardcoded-arn-partition': 'off',
132+
'partition/no-hardcoded-endpoint-tld': 'off',
71133
'@typescript-eslint/no-unsafe-assignment': 'off',
72134
'@typescript-eslint/no-unsafe-member-access': 'off',
73135
'@typescript-eslint/no-unsafe-call': 'off',

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,6 +1,7 @@
11
import { parseJsonRpcResponse } from '../../lib/utils/json-rpc';
22
import { getCredentialProvider } from './account';
33
import { parseAguiSSEStream } from './agui-parser';
4+
import { serviceEndpoint } from './partition';
45
import {
56
BedrockAgentCoreClient,
67
EvaluateCommand,
@@ -143,11 +144,10 @@ export function extractResult(text: string): string {
143144

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

153153
/**

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,

0 commit comments

Comments
 (0)