Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,23 @@ import type { ProviderSettings, OrganizationAllowList } from "@roo-code/types"

import { RouterModels } from "@roo/api"

import { getModelValidationError, validateApiConfigurationExcludingModelErrors } from "../validate"
// Mock i18next to return translation keys with interpolated values
vi.mock("i18next", () => ({
default: {
t: (key: string, options?: Record<string, string>) => {
if (options) {
let result = key
Object.entries(options).forEach(([k, v]) => {
result += ` ${k}=${v}`
})
return result
}
return key
},
},
}))

import { getModelValidationError, validateApiConfigurationExcludingModelErrors, validateBedrockArn } from "../validate"

describe("Model Validation Functions", () => {
const mockRouterModels: RouterModels = {
Expand Down Expand Up @@ -70,7 +86,7 @@ describe("Model Validation Functions", () => {
}

const result = getModelValidationError(config, mockRouterModels, allowAllOrganization)
expect(result).toBe("validation.modelAvailability")
expect(result).toContain("settings:validation.modelAvailability")
})

it("returns error for model not allowed by organization", () => {
Expand Down Expand Up @@ -100,7 +116,7 @@ describe("Model Validation Functions", () => {
}

const result = getModelValidationError(config, mockRouterModels, allowAllOrganization)
expect(result).toBe("validation.modelId")
expect(result).toBe("settings:validation.modelId")
})

it("handles undefined model IDs gracefully", () => {
Expand All @@ -110,7 +126,7 @@ describe("Model Validation Functions", () => {
}

const result = getModelValidationError(config, mockRouterModels, allowAllOrganization)
expect(result).toBe("validation.modelId")
expect(result).toBe("settings:validation.modelId")
})
})

Expand All @@ -134,7 +150,7 @@ describe("Model Validation Functions", () => {
}

const result = validateApiConfigurationExcludingModelErrors(config, mockRouterModels, allowAllOrganization)
expect(result).toBe("validation.apiKey")
expect(result).toBe("settings:validation.apiKey")
})

it("excludes model-specific errors", () => {
Expand Down Expand Up @@ -164,3 +180,92 @@ describe("Model Validation Functions", () => {
})
})
})

describe("validateBedrockArn", () => {
describe("always returns isValid: true (no strict format validation)", () => {
it("accepts standard AWS Bedrock ARNs", () => {
const result = validateBedrockArn(
"arn:aws:bedrock:us-west-2:123456789012:inference-profile/us.anthropic.claude-3-5-sonnet-v2",
)
expect(result.isValid).toBe(true)
expect(result.arnRegion).toBe("us-west-2")
expect(result.errorMessage).toBeUndefined()
})

it("accepts AWS GovCloud ARNs", () => {
const result = validateBedrockArn(
"arn:aws-us-gov:bedrock:us-gov-west-1:123456789012:inference-profile/model",
)
expect(result.isValid).toBe(true)
expect(result.arnRegion).toBe("us-gov-west-1")
expect(result.errorMessage).toBeUndefined()
})

it("accepts AWS China ARNs", () => {
const result = validateBedrockArn("arn:aws-cn:bedrock:cn-north-1:123456789012:inference-profile/model")
expect(result.isValid).toBe(true)
expect(result.arnRegion).toBe("cn-north-1")
expect(result.errorMessage).toBeUndefined()
})

it("accepts SageMaker ARNs", () => {
const result = validateBedrockArn("arn:aws:sagemaker:us-east-1:123456789012:endpoint/my-endpoint")
expect(result.isValid).toBe(true)
expect(result.arnRegion).toBe("us-east-1")
expect(result.errorMessage).toBeUndefined()
})

it("accepts non-standard ARN formats without validation errors", () => {
// Users are advanced - trust their input
const result = validateBedrockArn("arn:custom:service:region:account:resource")
expect(result.isValid).toBe(true)
expect(result.arnRegion).toBe("region")
expect(result.errorMessage).toBeUndefined()
})

it("accepts completely custom ARN strings", () => {
// Even unusual formats should be accepted
const result = validateBedrockArn("some-custom-arn-format")
expect(result.isValid).toBe(true)
// May not be able to extract region from non-standard format
expect(result.errorMessage).toBeUndefined()
})
})

describe("region mismatch warnings", () => {
it("shows warning when ARN region differs from provided region", () => {
const result = validateBedrockArn(
"arn:aws:bedrock:us-west-2:123456789012:inference-profile/model",
"us-east-1",
)
expect(result.isValid).toBe(true) // Still valid, just a warning
expect(result.arnRegion).toBe("us-west-2")
expect(result.errorMessage).toBeDefined()
expect(result.errorMessage).toContain("us-west-2")
})

it("shows no warning when ARN region matches provided region", () => {
const result = validateBedrockArn(
"arn:aws:bedrock:us-west-2:123456789012:inference-profile/model",
"us-west-2",
)
expect(result.isValid).toBe(true)
expect(result.arnRegion).toBe("us-west-2")
expect(result.errorMessage).toBeUndefined()
})

it("shows no warning when no region is provided to check against", () => {
const result = validateBedrockArn("arn:aws:bedrock:us-west-2:123456789012:inference-profile/model")
expect(result.isValid).toBe(true)
expect(result.arnRegion).toBe("us-west-2")
expect(result.errorMessage).toBeUndefined()
})

it("shows no warning when region cannot be extracted from ARN", () => {
const result = validateBedrockArn("non-arn-format", "us-east-1")
expect(result.isValid).toBe(true)
expect(result.arnRegion).toBeUndefined()
expect(result.errorMessage).toBeUndefined()
})
})
})
29 changes: 12 additions & 17 deletions webview-ui/src/utils/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,39 +216,34 @@ function getModelIdForProvider(apiConfiguration: ProviderSettings, provider: Pro
}

/**
* Validates an Amazon Bedrock ARN format and optionally checks if the region in
* Validates an Amazon Bedrock ARN and optionally checks if the region in
* the ARN matches the provided region.
*
* Note: This function does not perform strict format validation on the ARN.
* Users entering custom ARNs are advanced users who should be trusted to
* provide valid ARNs without restriction. See issue #10108.
*
* @param arn The ARN string to validate
* @param region Optional region to check against the ARN's region
* @returns An object with validation results: { isValid, arnRegion, errorMessage }
*/
export function validateBedrockArn(arn: string, region?: string) {
// Validate ARN format.
const arnRegex = /^arn:aws:(?:bedrock|sagemaker):([^:]+):([^:]*):(?:([^/]+)\/([\w.\-:]+)|([^/]+))$/
const match = arn.match(arnRegex)

if (!match) {
return {
isValid: false,
arnRegion: undefined,
errorMessage: i18next.t("settings:validation.arn.invalidFormat"),
}
}

// Extract region from ARN.
const arnRegion = match[1]
// Try to extract region from ARN for region mismatch warning.
// This is a permissive regex that attempts to find the region component
// without enforcing strict ARN format validation.
const regionMatch = arn.match(/^arn:[^:]+:[^:]+:([^:]+):/)
const arnRegion = regionMatch?.[1]

// Check if region in ARN matches provided region (if specified).
if (region && arnRegion !== region) {
if (region && arnRegion && arnRegion !== region) {
return {
isValid: true,
arnRegion,
errorMessage: i18next.t("settings:validation.arn.regionMismatch", { arnRegion, region }),
}
}

// ARN is valid and region matches (or no region was provided to check against).
// ARN is always considered valid - trust the user to enter valid ARNs.
return { isValid: true, arnRegion, errorMessage: undefined }
}

Expand Down
Loading