Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,44 @@ See `docs/TESTING.md` for details.
- Always look for existing types before creating a new type inline.
- Re-usable constants must be defined in a constants file in the closest sensible subdirectory.

## Multi-Partition Support (GovCloud, China)

The CLI supports multiple AWS partitions (commercial, GovCloud, China) through a central utility at
`src/cli/aws/partition.ts`. This module maps region prefixes to partition-specific values.

### Rules

- **Never hardcode `arn:aws:`** in ARN construction. Use `arnPrefix(region)` from `src/cli/aws/partition.ts`.
- **Never hardcode `amazonaws.com`** in endpoint URLs. Use `serviceEndpoint(service, region)` or `dnsSuffix(region)`.
- **Never hardcode `console.aws.amazon.com`** in console URLs. Use `consoleDomain(region)`.
- **ARN regex patterns** must use `arn:[^:]+:` (not `arn:aws:`) to match any partition.
- **Static JSON assets** (e.g., IAM policies in `src/assets/`) cannot use TypeScript utilities — use `arn:*:` as the
partition wildcard since IAM evaluates it across all partitions.

### Adding a New Region

Update these files in the CLI repo:

1. `src/schema/schemas/aws-targets.ts` — add to `AgentCoreRegionSchema` enum
2. `src/schema/llm-compacted/aws-targets.ts` — add to `AgentCoreRegion` type union
3. `src/schema/schemas/__tests__/aws-targets.test.ts` — add to `validRegions` array
4. `src/cli/operations/agent/import/constants.ts` — add to `BEDROCK_REGIONS` (if applicable to Bedrock Agent import)

Update these files in the CDK repo (`@aws/agentcore-cdk`):

5. `src/schema/schemas/aws-targets.ts` — add to `AgentCoreRegionSchema` enum
6. `src/schema/llm-compacted/aws-targets.ts` — add to `AgentCoreRegion` type union

Then run `npm run test:update-snapshots` in the CLI repo if any asset files changed.

### Adding a New Partition

1. Add a new entry to `PARTITION_CONFIGS` in `src/cli/aws/partition.ts` with the region prefix, partition name, DNS
suffix, and console domain.
2. Add tests for the new partition in `src/cli/aws/__tests__/partition.test.ts`.
3. Update `src/assets/cdk/cdk.json` — add the partition to `@aws-cdk/core:target-partitions`.
4. Run `npm run test:update-snapshots` to update asset snapshots.

## TUI Harness

See `docs/tui-harness.md` for the full TUI harness usage guide (MCP tools, screen markers, examples, and error
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ exports[`Assets Directory Snapshots > CDK assets > cdk/cdk/cdk.json should match
"@aws-cdk/aws-ecs-patterns:secGroupsDisablesImplicitOpenListener": true,
"@aws-cdk/aws-lambda:recognizeLayerVersion": true,
"@aws-cdk/core:checkSecretUsage": true,
"@aws-cdk/core:target-partitions": ["aws", "aws-cn"],
"@aws-cdk/core:target-partitions": ["aws", "aws-cn", "aws-us-gov"],
"@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true,
"@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true,
"@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true,
Expand Down
2 changes: 1 addition & 1 deletion src/assets/cdk/cdk.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"@aws-cdk/aws-ecs-patterns:secGroupsDisablesImplicitOpenListener": true,
"@aws-cdk/aws-lambda:recognizeLayerVersion": true,
"@aws-cdk/core:checkSecretUsage": true,
"@aws-cdk/core:target-partitions": ["aws", "aws-cn"],
"@aws-cdk/core:target-partitions": ["aws", "aws-cn", "aws-us-gov"],
"@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true,
"@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true,
"@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
{
"Effect": "Allow",
"Action": ["logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents"],
"Resource": "arn:aws:logs:*:*:log-group:/aws/lambda/*"
"Resource": "arn:*:logs:*:*:log-group:/aws/lambda/*"
}
]
}
76 changes: 76 additions & 0 deletions src/cli/aws/__tests__/partition.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { arnPrefix, consoleDomain, dnsSuffix, getPartition, serviceEndpoint } from '../partition';
import { describe, expect, it } from 'vitest';

describe('getPartition', () => {
it('returns aws for standard commercial regions', () => {
expect(getPartition('us-east-1')).toBe('aws');
expect(getPartition('eu-west-1')).toBe('aws');
expect(getPartition('ap-southeast-1')).toBe('aws');
});

it('returns aws-us-gov for GovCloud regions', () => {
expect(getPartition('us-gov-west-1')).toBe('aws-us-gov');
expect(getPartition('us-gov-east-1')).toBe('aws-us-gov');
});

it('returns aws-cn for China regions', () => {
expect(getPartition('cn-north-1')).toBe('aws-cn');
expect(getPartition('cn-northwest-1')).toBe('aws-cn');
});
});

describe('arnPrefix', () => {
it('returns arn:aws for commercial regions', () => {
expect(arnPrefix('us-east-1')).toBe('arn:aws');
});

it('returns arn:aws-us-gov for GovCloud regions', () => {
expect(arnPrefix('us-gov-west-1')).toBe('arn:aws-us-gov');
});

it('returns arn:aws-cn for China regions', () => {
expect(arnPrefix('cn-north-1')).toBe('arn:aws-cn');
});
});

describe('dnsSuffix', () => {
it('returns amazonaws.com for commercial regions', () => {
expect(dnsSuffix('us-east-1')).toBe('amazonaws.com');
});

it('returns amazonaws.com for GovCloud regions', () => {
expect(dnsSuffix('us-gov-west-1')).toBe('amazonaws.com');
});

it('returns amazonaws.com.cn for China regions', () => {
expect(dnsSuffix('cn-north-1')).toBe('amazonaws.com.cn');
});
});

describe('serviceEndpoint', () => {
it('builds correct endpoint for commercial regions', () => {
expect(serviceEndpoint('bedrock-agentcore', 'us-east-1')).toBe('bedrock-agentcore.us-east-1.amazonaws.com');
});

it('builds correct endpoint for GovCloud regions', () => {
expect(serviceEndpoint('bedrock-agentcore', 'us-gov-west-1')).toBe('bedrock-agentcore.us-gov-west-1.amazonaws.com');
});

it('builds correct endpoint for China regions', () => {
expect(serviceEndpoint('bedrock-agentcore', 'cn-north-1')).toBe('bedrock-agentcore.cn-north-1.amazonaws.com.cn');
});
});

describe('consoleDomain', () => {
it('returns console.aws.amazon.com for commercial regions', () => {
expect(consoleDomain('us-east-1')).toBe('console.aws.amazon.com');
});

it('returns console.amazonaws-us-gov.com for GovCloud regions', () => {
expect(consoleDomain('us-gov-west-1')).toBe('console.amazonaws-us-gov.com');
});

it('returns console.amazonaws.cn for China regions', () => {
expect(consoleDomain('cn-north-1')).toBe('console.amazonaws.cn');
});
});
4 changes: 2 additions & 2 deletions src/cli/aws/agentcore.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { parseJsonRpcResponse } from '../../lib/utils/json-rpc';
import { getCredentialProvider } from './account';
import { serviceEndpoint } from './partition';
import {
BedrockAgentCoreClient,
EvaluateCommand,
Expand Down Expand Up @@ -142,11 +143,10 @@ export function extractResult(text: string): string {

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

/**
Expand Down
2 changes: 1 addition & 1 deletion src/cli/aws/bedrock-import.ts
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,7 @@ async function fetchCollaborators(
const aliasArn = (summary as { agentDescriptor?: { aliasArn?: string } }).agentDescriptor?.aliasArn;
if (!aliasArn) continue;

const arnMatch = /^arn:aws:bedrock:[^:]+:[^:]+:agent-alias\/([^/]+)\/([^/]+)$/.exec(aliasArn);
const arnMatch = /^arn:[^:]+:bedrock:[^:]+:[^:]+:agent-alias\/([^/]+)\/([^/]+)$/.exec(aliasArn);
if (!arnMatch) continue;
const [, collabAgentId, collabAliasId] = arnMatch;
if (!collabAgentId || !collabAliasId) continue;
Expand Down
3 changes: 2 additions & 1 deletion src/cli/aws/cloudwatch.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { getCredentialProvider } from './account';
import { arnPrefix } from './partition';
import { CloudWatchLogsClient, FilterLogEventsCommand, StartLiveTailCommand } from '@aws-sdk/client-cloudwatch-logs';

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

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

while (!abortSignal?.aborted) {
const client = new CloudWatchLogsClient({
Expand Down
1 change: 1 addition & 0 deletions src/cli/aws/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export { detectAwsContext, type AwsContext } from './aws-context';
export { detectAccount, getCredentialProvider } from './account';
export { getPartition, arnPrefix, dnsSuffix, serviceEndpoint, consoleDomain } from './partition';
export { detectRegion, type RegionDetectionResult } from './region';
export {
invokeBedrockSync,
Expand Down
28 changes: 28 additions & 0 deletions src/cli/aws/partition.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { partition } from '@aws-sdk/util-endpoints';

const CONSOLE_DOMAINS: Record<string, string> = {
'aws-us-gov': 'console.amazonaws-us-gov.com',
'aws-cn': 'console.amazonaws.cn',
};

const DEFAULT_CONSOLE_DOMAIN = 'console.aws.amazon.com';

export function getPartition(region: string): string {
return partition(region).name;
}

export function arnPrefix(region: string): string {
return `arn:${getPartition(region)}`;
}

export function dnsSuffix(region: string): string {
return partition(region).dnsSuffix;
}

export function serviceEndpoint(service: string, region: string): string {
return `${service}.${region}.${dnsSuffix(region)}`;
}

export function consoleDomain(region: string): string {
return CONSOLE_DOMAINS[getPartition(region)] ?? DEFAULT_CONSOLE_DOMAIN;
}
7 changes: 4 additions & 3 deletions src/cli/aws/transaction-search.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { getErrorMessage, isAccessDeniedError } from '../errors';
import { getCredentialProvider } from './account';
import { arnPrefix } from './partition';
import { ApplicationSignalsClient, StartDiscoveryCommand } from '@aws-sdk/client-application-signals';
import {
CloudWatchLogsClient,
Expand Down Expand Up @@ -64,11 +65,11 @@ export async function enableTransactionSearch(
Principal: { Service: 'xray.amazonaws.com' },
Action: 'logs:PutLogEvents',
Resource: [
`arn:aws:logs:${region}:${accountId}:log-group:aws/spans:*`,
`arn:aws:logs:${region}:${accountId}:log-group:/aws/application-signals/data:*`,
`${arnPrefix(region)}:logs:${region}:${accountId}:log-group:aws/spans:*`,
`${arnPrefix(region)}:logs:${region}:${accountId}:log-group:/aws/application-signals/data:*`,
],
Condition: {
ArnLike: { 'aws:SourceArn': `arn:aws:xray:${region}:${accountId}:*` },
ArnLike: { 'aws:SourceArn': `${arnPrefix(region)}:xray:${region}:${accountId}:*` },
StringEquals: { 'aws:SourceAccount': accountId },
},
},
Expand Down
5 changes: 3 additions & 2 deletions src/cli/commands/import/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type {
Memory,
} from '../../../schema';
import { validateAwsCredentials } from '../../aws/account';
import { arnPrefix } from '../../aws/partition';
import { ExecLogger } from '../../logging';
import { setupPythonProject } from '../../operations/python/setup';
import { executeCdkImportPipeline } from './import-pipeline';
Expand Down Expand Up @@ -521,7 +522,7 @@ export async function handleImport(options: ImportOptions): Promise<ImportResult
id: a.physicalAgentId!,
arn:
a.physicalAgentArn ??
`arn:aws:bedrock-agentcore:${target.region}:${target.account}:runtime/${a.physicalAgentId}`,
`${arnPrefix(target.region)}:bedrock-agentcore:${target.region}:${target.account}:runtime/${a.physicalAgentId}`,
})),
...memoriesToImport
.filter(m => m.physicalMemoryId)
Expand All @@ -531,7 +532,7 @@ export async function handleImport(options: ImportOptions): Promise<ImportResult
id: m.physicalMemoryId!,
arn:
m.physicalMemoryArn ??
`arn:aws:bedrock-agentcore:${target.region}:${target.account}:memory/${m.physicalMemoryId}`,
`${arnPrefix(target.region)}:bedrock-agentcore:${target.region}:${target.account}:memory/${m.physicalMemoryId}`,
})),
];

Expand Down
3 changes: 2 additions & 1 deletion src/cli/commands/import/import-online-eval.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
listAllAgentRuntimes,
listAllOnlineEvaluationConfigs,
} from '../../aws/agentcore-control';
import { arnPrefix } from '../../aws/partition';
import { ANSI } from './constants';
import { failResult, findResourceInDeployedState, parseAndValidateArn } from './import-utils';
import { executeResourceImport } from './resource-import';
Expand Down Expand Up @@ -52,7 +53,7 @@ export function toOnlineEvalConfigSpec(
* since evaluators locked by an online eval config cannot be CFN-imported.
*/
function buildEvaluatorArns(evaluatorIds: string[], region: string, account: string): string[] {
return evaluatorIds.map(id => `arn:aws:bedrock-agentcore:${region}:${account}:evaluator/${id}`);
return evaluatorIds.map(id => `${arnPrefix(region)}:bedrock-agentcore:${region}:${account}:evaluator/${id}`);
}

/**
Expand Down
10 changes: 5 additions & 5 deletions src/cli/commands/import/import-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,10 +130,10 @@ export async function resolveImportTarget(options: ResolveTargetOptions): Promis
// Validate ARN format early if provided
if (
arn &&
!/^arn:aws:bedrock-agentcore:([^:]+):([^:]+):(runtime|memory|evaluator|online-evaluation-config)\/(.+)$/.test(arn)
!/^arn:[^:]+:bedrock-agentcore:([^:]+):([^:]+):(runtime|memory|evaluator|online-evaluation-config)\/(.+)$/.test(arn)
) {
throw new Error(
`Not a valid ARN: "${arn}".\nExpected format: arn:aws:bedrock-agentcore:<region>:<account>:<runtime|memory|evaluator|online-evaluation-config>/<id>`
`Not a valid ARN: "${arn}".\nExpected format: arn:<partition>:bedrock-agentcore:<region>:<account>:<runtime|memory|evaluator|online-evaluation-config>/<id>`
);
}

Expand All @@ -146,7 +146,7 @@ export async function resolveImportTarget(options: ResolveTargetOptions): Promis
);
}

const arnMatch = /^arn:aws:bedrock-agentcore:([^:]+):([^:]+):/.exec(arn);
const arnMatch = /^arn:[^:]+:bedrock-agentcore:([^:]+):([^:]+):/.exec(arn);
if (!arnMatch) {
throw new Error(
'No deployment targets found in project and could not parse region/account from ARN.\nRun `agentcore deploy` first to set up a target, then re-run import.'
Expand Down Expand Up @@ -210,7 +210,7 @@ export interface ParsedArn {
}

const ARN_PATTERN =
/^arn:aws:bedrock-agentcore:([^:]+):([^:]+):(runtime|memory|evaluator|online-evaluation-config)\/(.+)$/;
/^arn:[^:]+:bedrock-agentcore:([^:]+):([^:]+):(runtime|memory|evaluator|online-evaluation-config)\/(.+)$/;

/** Unified config for each importable resource type — ARN mapping, deployed state keys. */
const RESOURCE_TYPE_CONFIG: Record<
Expand Down Expand Up @@ -244,7 +244,7 @@ export function parseAndValidateArn(
const expectedArnType = RESOURCE_TYPE_CONFIG[expectedResourceType].arnType;
if (!match) {
throw new Error(
`Invalid ARN format: "${arn}". Expected format: arn:aws:bedrock-agentcore:<region>:<account>:${expectedArnType}/<id>`
`Invalid ARN format: "${arn}". Expected format: arn:<partition>:bedrock-agentcore:<region>:<account>:${expectedArnType}/<id>`
);
}

Expand Down
3 changes: 2 additions & 1 deletion src/cli/commands/status/constants.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { serviceEndpoint } from '../../aws/partition';
import { STATUS_COLORS } from '../../tui/theme';

export type ResourceDeploymentState = 'deployed' | 'local-only' | 'pending-removal';
Expand All @@ -16,5 +17,5 @@ export const DEPLOYMENT_STATE_LABELS: Record<ResourceDeploymentState, string> =

export function buildRuntimeInvocationUrl(region: string, runtimeArn: string): string {
const encodedArn = encodeURIComponent(runtimeArn);
return `https://bedrock-agentcore.${region}.amazonaws.com/runtimes/${encodedArn}/invocations`;
return `https://${serviceEndpoint('bedrock-agentcore', region)}/runtimes/${encodedArn}/invocations`;
}
5 changes: 4 additions & 1 deletion src/cli/operations/agent/import/base-translator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import type {
KnowledgeBaseInfo,
PromptConfiguration,
} from '../../../aws/bedrock-import-types';
import { arnPrefix } from '../../../aws/partition';
import type { MemoryOption } from '../../../tui/screens/generate/types';

export interface TranslatorOptions {
Expand Down Expand Up @@ -373,7 +374,9 @@ memory_id = os.environ.get("MEMORY_ID", "")
if (kb.knowledgeBaseArn) {
kbArns.push(kb.knowledgeBaseArn);
} else if (kb.knowledgeBaseId) {
kbArns.push(`arn:aws:bedrock:${this.agentRegion}:*:knowledge-base/${kb.knowledgeBaseId}`);
kbArns.push(
`${arnPrefix(this.agentRegion)}:bedrock:${this.agentRegion}:*:knowledge-base/${kb.knowledgeBaseId}`
);
}
}

Expand Down
1 change: 1 addition & 0 deletions src/cli/operations/agent/import/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export const BEDROCK_REGIONS = [
{ id: 'ap-south-1', title: 'Asia Pacific (Mumbai)' },
{ id: 'ca-central-1', title: 'Canada (Central)' },
{ id: 'sa-east-1', title: 'South America (Sao Paulo)' },
{ id: 'us-gov-west-1', title: 'GovCloud (US West)' },
] as const;

export const IMPORT_FRAMEWORK_OPTIONS = [
Expand Down
5 changes: 3 additions & 2 deletions src/cli/operations/traces/trace-url.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { arnPrefix, consoleDomain } from '../../aws/partition';
import { DEFAULT_ENDPOINT_NAME } from '../../constants';

/**
Expand All @@ -11,7 +12,7 @@ export function buildTraceConsoleUrl(params: {
}): string {
const { region, accountId, runtimeId, agentName } = params;
const resourceId = encodeURIComponent(
`arn:aws:bedrock-agentcore:${region}:${accountId}:runtime/${runtimeId}/runtime-endpoint/${DEFAULT_ENDPOINT_NAME}:${DEFAULT_ENDPOINT_NAME}`
`${arnPrefix(region)}:bedrock-agentcore:${region}:${accountId}:runtime/${runtimeId}/runtime-endpoint/${DEFAULT_ENDPOINT_NAME}:${DEFAULT_ENDPOINT_NAME}`
);
return `https://${region}.console.aws.amazon.com/cloudwatch/home?region=${region}#/gen-ai-observability/agent-core/agent-alias/${runtimeId}/endpoint/${DEFAULT_ENDPOINT_NAME}/agent/${agentName}?start=-43200000&resourceId=${resourceId}&serviceName=${agentName}.${DEFAULT_ENDPOINT_NAME}&tabId=traces`;
return `https://${region}.${consoleDomain(region)}/cloudwatch/home?region=${region}#/gen-ai-observability/agent-core/agent-alias/${runtimeId}/endpoint/${DEFAULT_ENDPOINT_NAME}/agent/${agentName}?start=-43200000&resourceId=${resourceId}&serviceName=${agentName}.${DEFAULT_ENDPOINT_NAME}&tabId=traces`;
}
Loading
Loading