fix: support AWS GovCloud and China region ARNs in Bedrock validation

- Updated ARN regex in validateBedrockArn to support aws-us-gov and aws-cn partitions
- Updated ARN regex in bedrock.ts parseArn method to support GovCloud and China regions
- Added comprehensive tests for GovCloud and China ARN validation
- Fixes #9481
This commit is contained in:
Roo Code 2025-11-21 18:23:08 +00:00
parent 852e293610
commit ad3ad95af2
5 changed files with 351 additions and 3 deletions

View file

@ -360,6 +360,158 @@ describe("AwsBedrockHandler", () => {
expect(result.modelId).toBe("ap.anthropic.claude-3-5-sonnet-20241022-v2:0") // Should be preserved as-is
})
})
describe("GovCloud and China partition ARN parsing", () => {
it("should parse GovCloud ARNs correctly", () => {
const handler = new AwsBedrockHandler({
apiModelId: "test",
awsAccessKey: "test",
awsSecretKey: "test",
awsRegion: "us-gov-west-1",
})
const parseArn = (handler as any).parseArn.bind(handler)
const result = parseArn(
"arn:aws-us-gov:bedrock:us-gov-west-1:123456789012:inference-profile/us-gov.anthropic.claude-sonnet-4-5-20250929-v1:0",
)
expect(result.isValid).toBe(true)
expect(result.region).toBe("us-gov-west-1")
expect(result.modelType).toBe("inference-profile")
expect(result.modelId).toBe("us-gov.anthropic.claude-sonnet-4-5-20250929-v1:0")
})
it("should parse GovCloud ARNs without account ID", () => {
const handler = new AwsBedrockHandler({
apiModelId: "test",
awsAccessKey: "test",
awsSecretKey: "test",
awsRegion: "us-gov-east-1",
})
const parseArn = (handler as any).parseArn.bind(handler)
const result = parseArn("arn:aws-us-gov:bedrock:us-gov-east-1::foundation-model/anthropic.claude-v2")
expect(result.isValid).toBe(true)
expect(result.region).toBe("us-gov-east-1")
expect(result.modelType).toBe("foundation-model")
expect(result.modelId).toBe("anthropic.claude-v2")
})
it("should parse China region ARNs correctly", () => {
const handler = new AwsBedrockHandler({
apiModelId: "test",
awsAccessKey: "test",
awsSecretKey: "test",
awsRegion: "cn-north-1",
})
const parseArn = (handler as any).parseArn.bind(handler)
const result = parseArn(
"arn:aws-cn:bedrock:cn-north-1:123456789012:foundation-model/anthropic.claude-3-sonnet-20240229-v1:0",
)
expect(result.isValid).toBe(true)
expect(result.region).toBe("cn-north-1")
expect(result.modelType).toBe("foundation-model")
expect(result.modelId).toBe("anthropic.claude-3-sonnet-20240229-v1:0")
})
it("should parse China region ARNs without account ID", () => {
const handler = new AwsBedrockHandler({
apiModelId: "test",
awsAccessKey: "test",
awsSecretKey: "test",
awsRegion: "cn-northwest-1",
})
const parseArn = (handler as any).parseArn.bind(handler)
const result = parseArn("arn:aws-cn:bedrock:cn-northwest-1::inference-profile/custom-model")
expect(result.isValid).toBe(true)
expect(result.region).toBe("cn-northwest-1")
expect(result.modelType).toBe("inference-profile")
expect(result.modelId).toBe("custom-model")
})
it("should handle GovCloud Sagemaker ARNs", () => {
const handler = new AwsBedrockHandler({
apiModelId: "test",
awsAccessKey: "test",
awsSecretKey: "test",
awsRegion: "us-gov-west-1",
})
const parseArn = (handler as any).parseArn.bind(handler)
const result = parseArn("arn:aws-us-gov:sagemaker:us-gov-west-1:123456789012:endpoint/gov-endpoint")
expect(result.isValid).toBe(true)
expect(result.region).toBe("us-gov-west-1")
expect(result.modelType).toBe("endpoint")
expect(result.modelId).toBe("gov-endpoint")
})
it("should handle China Sagemaker ARNs", () => {
const handler = new AwsBedrockHandler({
apiModelId: "test",
awsAccessKey: "test",
awsSecretKey: "test",
awsRegion: "cn-north-1",
})
const parseArn = (handler as any).parseArn.bind(handler)
const result = parseArn("arn:aws-cn:sagemaker:cn-north-1:123456789012:endpoint/china-endpoint")
expect(result.isValid).toBe(true)
expect(result.region).toBe("cn-north-1")
expect(result.modelType).toBe("endpoint")
expect(result.modelId).toBe("china-endpoint")
})
it("should detect region mismatch for GovCloud ARNs", () => {
const handler = new AwsBedrockHandler({
apiModelId: "test",
awsAccessKey: "test",
awsSecretKey: "test",
awsRegion: "us-gov-west-1",
})
const parseArn = (handler as any).parseArn.bind(handler)
const result = parseArn(
"arn:aws-us-gov:bedrock:us-gov-east-1::foundation-model/anthropic.claude-v2",
"us-gov-west-1",
)
expect(result.isValid).toBe(true)
expect(result.region).toBe("us-gov-east-1")
expect(result.errorMessage).toContain("region")
expect(result.errorMessage).toContain("us-gov-east-1")
expect(result.errorMessage).toContain("us-gov-west-1")
})
it("should reject invalid partition ARNs", () => {
const handler = new AwsBedrockHandler({
apiModelId: "test",
awsAccessKey: "test",
awsSecretKey: "test",
awsRegion: "us-east-1",
})
const parseArn = (handler as any).parseArn.bind(handler)
const result = parseArn("arn:aws-invalid:bedrock:us-east-1::foundation-model/anthropic.claude-v2")
expect(result.isValid).toBe(false)
expect(result.errorMessage).toContain("Invalid ARN format")
})
})
})
describe("image handling", () => {

View file

@ -808,6 +808,8 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH
*
* This matches ARNs like:
* - Foundation Model: arn:aws:bedrock:us-west-2::foundation-model/anthropic.claude-v2
* - GovCloud Foundation Model: arn:aws-us-gov:bedrock:us-gov-west-1::foundation-model/anthropic.claude-v2
* - China Foundation Model: arn:aws-cn:bedrock:cn-north-1::foundation-model/anthropic.claude-v2
* - Prompt Router: arn:aws:bedrock:us-west-2:123456789012:prompt-router/anthropic-claude
* - Inference Profile: arn:aws:bedrock:us-west-2:123456789012:inference-profile/anthropic.claude-v2
* - Cross Region Inference Profile: arn:aws:bedrock:us-west-2:123456789012:inference-profile/us.anthropic.claude-3-5-sonnet-20241022-v2:0
@ -815,13 +817,15 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH
* - Imported Model: arn:aws:bedrock:us-west-2:123456789012:imported-model/my-imported-model
*
* match[0] - The entire matched string
* match[1] - The region (e.g., "us-east-1")
* match[1] - The region (e.g., "us-east-1", "us-gov-west-1", "cn-north-1")
* match[2] - The account ID (can be empty string for AWS-managed resources)
* match[3] - The resource type (e.g., "foundation-model")
* match[4] - The resource ID (e.g., "anthropic.claude-3-sonnet-20240229-v1:0")
*/
const arnRegex = /^arn:aws:(?:bedrock|sagemaker):([^:]+):([^:]*):(?:([^\/]+)\/([\w\.\-:]+)|([^\/]+))$/
// Support standard AWS (aws), GovCloud (aws-us-gov), and China (aws-cn) partitions
const arnRegex =
/^arn:(?:aws|aws-us-gov|aws-cn):(?:bedrock|sagemaker):([^:]+):([^:]*):(?:([^\/]+)\/([\w\.\-:]+)|([^\/]+))$/
let match = arn.match(arnRegex)
if (match && match[1] && match[3] && match[4]) {

View file

@ -0,0 +1,190 @@
import { vi } from "vitest"
import { validateBedrockArn } from "../validate"
// Mock i18next to return predictable error messages
vi.mock("i18next", () => ({
default: {
t: (key: string, params?: any) => {
if (key === "settings:validation.arn.invalidFormat") {
return "Invalid ARN format"
}
if (key === "settings:validation.arn.regionMismatch") {
return `Region mismatch: ARN region ${params?.arnRegion} does not match ${params?.region}`
}
return key
},
},
}))
describe("validateBedrockArn", () => {
describe("Standard AWS partition ARNs", () => {
it("should validate standard AWS Bedrock ARNs", () => {
const arn = "arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-3-sonnet-20240229-v1:0"
const result = validateBedrockArn(arn)
expect(result.isValid).toBe(true)
expect(result.arnRegion).toBe("us-east-1")
expect(result.errorMessage).toBeUndefined()
})
it("should validate ARNs with account IDs", () => {
const arn = "arn:aws:bedrock:us-west-2:123456789012:inference-profile/custom-profile"
const result = validateBedrockArn(arn)
expect(result.isValid).toBe(true)
expect(result.arnRegion).toBe("us-west-2")
expect(result.errorMessage).toBeUndefined()
})
it("should validate Sagemaker ARNs", () => {
const arn = "arn:aws:sagemaker:eu-west-1:123456789012:endpoint/my-endpoint"
const result = validateBedrockArn(arn)
expect(result.isValid).toBe(true)
expect(result.arnRegion).toBe("eu-west-1")
expect(result.errorMessage).toBeUndefined()
})
})
describe("AWS GovCloud partition ARNs", () => {
it("should validate GovCloud Bedrock ARNs", () => {
const arn =
"arn:aws-us-gov:bedrock:us-gov-west-1:123456789012:inference-profile/us-gov.anthropic.claude-sonnet-4-5-20250929-v1:0"
const result = validateBedrockArn(arn)
expect(result.isValid).toBe(true)
expect(result.arnRegion).toBe("us-gov-west-1")
expect(result.errorMessage).toBeUndefined()
})
it("should validate GovCloud ARNs without account ID", () => {
const arn = "arn:aws-us-gov:bedrock:us-gov-east-1::foundation-model/anthropic.claude-v2"
const result = validateBedrockArn(arn)
expect(result.isValid).toBe(true)
expect(result.arnRegion).toBe("us-gov-east-1")
expect(result.errorMessage).toBeUndefined()
})
it("should validate GovCloud Sagemaker ARNs", () => {
const arn = "arn:aws-us-gov:sagemaker:us-gov-west-1:123456789012:endpoint/gov-endpoint"
const result = validateBedrockArn(arn)
expect(result.isValid).toBe(true)
expect(result.arnRegion).toBe("us-gov-west-1")
expect(result.errorMessage).toBeUndefined()
})
})
describe("AWS China partition ARNs", () => {
it("should validate China Bedrock ARNs", () => {
const arn = "arn:aws-cn:bedrock:cn-north-1:123456789012:foundation-model/anthropic.claude-v2"
const result = validateBedrockArn(arn)
expect(result.isValid).toBe(true)
expect(result.arnRegion).toBe("cn-north-1")
expect(result.errorMessage).toBeUndefined()
})
it("should validate China ARNs without account ID", () => {
const arn = "arn:aws-cn:bedrock:cn-northwest-1::inference-profile/custom-model"
const result = validateBedrockArn(arn)
expect(result.isValid).toBe(true)
expect(result.arnRegion).toBe("cn-northwest-1")
expect(result.errorMessage).toBeUndefined()
})
it("should validate China Sagemaker ARNs", () => {
const arn = "arn:aws-cn:sagemaker:cn-north-1:123456789012:endpoint/china-endpoint"
const result = validateBedrockArn(arn)
expect(result.isValid).toBe(true)
expect(result.arnRegion).toBe("cn-north-1")
expect(result.errorMessage).toBeUndefined()
})
})
describe("Region validation", () => {
it("should detect region mismatch for standard AWS", () => {
const arn = "arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-v2"
const result = validateBedrockArn(arn, "us-west-2")
expect(result.isValid).toBe(true)
expect(result.arnRegion).toBe("us-east-1")
// Error message should be defined when there's a region mismatch
expect(result.errorMessage).toBeDefined()
})
it("should detect region mismatch for GovCloud", () => {
const arn = "arn:aws-us-gov:bedrock:us-gov-west-1::foundation-model/anthropic.claude-v2"
const result = validateBedrockArn(arn, "us-gov-east-1")
expect(result.isValid).toBe(true)
expect(result.arnRegion).toBe("us-gov-west-1")
// Error message should be defined when there's a region mismatch
expect(result.errorMessage).toBeDefined()
})
it("should detect region mismatch for China", () => {
const arn = "arn:aws-cn:bedrock:cn-north-1::foundation-model/anthropic.claude-v2"
const result = validateBedrockArn(arn, "cn-northwest-1")
expect(result.isValid).toBe(true)
expect(result.arnRegion).toBe("cn-north-1")
// Error message should be defined when there's a region mismatch
expect(result.errorMessage).toBeDefined()
})
it("should pass when regions match", () => {
const arn = "arn:aws-us-gov:bedrock:us-gov-west-1::foundation-model/anthropic.claude-v2"
const result = validateBedrockArn(arn, "us-gov-west-1")
expect(result.isValid).toBe(true)
expect(result.arnRegion).toBe("us-gov-west-1")
expect(result.errorMessage).toBeUndefined()
})
})
describe("Invalid ARN formats", () => {
it("should reject invalid ARN format", () => {
const arn = "not-an-arn"
const result = validateBedrockArn(arn)
expect(result.isValid).toBe(false)
expect(result.arnRegion).toBeUndefined()
// Error message should be defined for invalid ARN
expect(result.errorMessage).toBeDefined()
})
it("should reject ARN with invalid partition", () => {
const arn = "arn:aws-invalid:bedrock:us-east-1::foundation-model/anthropic.claude-v2"
const result = validateBedrockArn(arn)
expect(result.isValid).toBe(false)
expect(result.arnRegion).toBeUndefined()
// Error message should be defined for invalid ARN
expect(result.errorMessage).toBeDefined()
})
it("should reject ARN with invalid service", () => {
const arn = "arn:aws:invalid-service:us-east-1::foundation-model/anthropic.claude-v2"
const result = validateBedrockArn(arn)
expect(result.isValid).toBe(false)
expect(result.arnRegion).toBeUndefined()
// Error message should be defined for invalid ARN
expect(result.errorMessage).toBeDefined()
})
it("should reject ARN missing resource", () => {
const arn = "arn:aws:bedrock:us-east-1:123456789012:"
const result = validateBedrockArn(arn)
expect(result.isValid).toBe(false)
expect(result.arnRegion).toBeUndefined()
// Error message should be defined for invalid ARN
expect(result.errorMessage).toBeDefined()
})
})
})

View file

@ -225,7 +225,9 @@ function getModelIdForProvider(apiConfiguration: ProviderSettings, provider: Pro
*/
export function validateBedrockArn(arn: string, region?: string) {
// Validate ARN format.
const arnRegex = /^arn:aws:(?:bedrock|sagemaker):([^:]+):([^:]*):(?:([^/]+)\/([\w.\-:]+)|([^/]+))$/
// Support standard AWS (aws), GovCloud (aws-us-gov), and China (aws-cn) partitions
const arnRegex =
/^arn:(?:aws|aws-us-gov|aws-cn):(?:bedrock|sagemaker):([^:]+):([^:]*):(?:([^/]+)\/([\w.\-:]+)|([^/]+))$/
const match = arn.match(arnRegex)
if (!match) {