diff --git a/src/core/abTestExecutionRole.tsx b/src/core/abTestExecutionRole.tsx index 721f4d026..458db3a78 100644 --- a/src/core/abTestExecutionRole.tsx +++ b/src/core/abTestExecutionRole.tsx @@ -7,6 +7,7 @@ import { type IAMClient, } from "@aws-sdk/client-iam"; import { createHash } from "node:crypto"; +import { parseArn, resourceNameFromArn } from "./arn"; const AB_TEST_POLICY_NAME = "ABTestExecutionPolicy"; @@ -16,13 +17,8 @@ export function abTestExecutionRoleName(testName: string): string { return `${base.slice(0, 55)}-${hash}`; } -export function roleNameFromArn(roleArn: string): string { - const parts = roleArn.split("/"); - return parts[parts.length - 1] ?? roleArn; -} - export function accountIdFromArn(arn: string): string { - const accountId = arn.split(":")[4]; + const accountId = parseArn(arn)?.account; if (!accountId) throw new Error(`could not extract account id from ARN: ${arn}`); return accountId; } @@ -139,7 +135,7 @@ export async function provisionAbTestRole( } export async function deleteAbTestRole(iam: IAMClient, roleArn: string): Promise { - const roleName = roleNameFromArn(roleArn); + const roleName = resourceNameFromArn(roleArn); try { await iam.send( new DeleteRolePolicyCommand({ RoleName: roleName, PolicyName: AB_TEST_POLICY_NAME }), diff --git a/src/core/eval.tsx b/src/core/eval.tsx index 34ad58774..bf9080d51 100644 --- a/src/core/eval.tsx +++ b/src/core/eval.tsx @@ -172,10 +172,10 @@ import { grantOnlineEvalScope, isManagedOnlineEvalRole, revokeOnlineEvalScope, - roleNameFromArn, scopePolicyName, } from "./onlineEvalExecutionRole"; import { accountIdFromArn, deleteAbTestRole, provisionAbTestRole } from "./abTestExecutionRole"; +import { resourceNameFromArn } from "./arn"; import { harnessRuntimeFromResponse } from "./harness"; const DEFAULT_INGESTION_WAIT_MS = 180_000; @@ -1289,7 +1289,7 @@ export class EvalClient implements CoreEvalClient { options.region, newLogGroups, kmsKeys, - roleNameFromArn(roleArn!), + resourceNameFromArn(roleArn!), ); const oldPolicyName = scopePolicyName( executionPolicy( @@ -1312,7 +1312,7 @@ export class EvalClient implements CoreEvalClient { if (newPolicyName !== oldPolicyName) { const revoked = await revokeOnlineEvalScope( iam, - roleNameFromArn(managedRoleArn), + resourceNameFromArn(managedRoleArn), oldPolicyName, ).catch(() => false); // The config is already correct; the role just still grants a data diff --git a/src/core/executionRole.tsx b/src/core/executionRole.tsx index 0a7581019..e93bec34a 100644 --- a/src/core/executionRole.tsx +++ b/src/core/executionRole.tsx @@ -4,6 +4,7 @@ import { PutRolePolicyCommand, type IAMClient, } from "@aws-sdk/client-iam"; +import { parseArn } from "./arn"; // Default harness execution role provisioning. // @@ -223,7 +224,7 @@ function executionPolicy(region: string, accountId: string, harnessName: string) // (arn:aws:iam:::role/), which saves an STS lookup: the account // only becomes relevant once we hold the role's ARN anyway. function accountIdFromRoleArn(arn: string): string { - const accountId = arn.split(":")[4]; + const accountId = parseArn(arn)?.account; if (!accountId) { throw new Error(`Cannot extract an account id from role ARN "${arn}"`); } diff --git a/src/core/onlineEvalExecutionRole.tsx b/src/core/onlineEvalExecutionRole.tsx index 4f15beb01..e02a6f40a 100644 --- a/src/core/onlineEvalExecutionRole.tsx +++ b/src/core/onlineEvalExecutionRole.tsx @@ -6,6 +6,7 @@ import { PutRolePolicyCommand, type IAMClient, } from "@aws-sdk/client-iam"; +import { parseArn, resourceNameFromArn } from "./arn"; // Default online-evaluation execution role provisioning, mirroring // core/executionRole.tsx's pattern for harnesses: CreateOnlineEvaluationConfig @@ -49,15 +50,11 @@ function truncatedRolePrefix(configName: string): string { return `${ROLE_NAME_PREFIX}${configName.slice(0, room)}-`; } -export function roleNameFromArn(roleArn: string): string { - return roleArn.slice(roleArn.lastIndexOf("/") + 1); -} - // isManagedOnlineEvalRole recognises the CLI's default role for a config. Roles // created before the hash moved off Bun.hash carry a different suffix, so a // truncated name is matched on its prefix rather than recomputed. export function isManagedOnlineEvalRole(roleArn: string, configName: string): boolean { - const roleName = roleNameFromArn(roleArn); + const roleName = resourceNameFromArn(roleArn); const full = `${ROLE_NAME_PREFIX}${configName}`; if (full.length <= ROLE_NAME_MAX) return roleName === full; return roleName.startsWith(truncatedRolePrefix(configName)); @@ -188,7 +185,7 @@ export function executionPolicy( } export function accountIdFromRoleArn(arn: string): string { - const accountId = arn.split(":")[4]; + const accountId = parseArn(arn)?.account; if (!accountId) { throw new Error(`Cannot extract an account id from role ARN "${arn}"`); } diff --git a/src/core/policy.tsx b/src/core/policy.tsx index efbf663a5..9003eafb3 100644 --- a/src/core/policy.tsx +++ b/src/core/policy.tsx @@ -22,6 +22,7 @@ import type { import type { Logger } from "../logging"; import type { ProgressEvent } from "../tui/progress"; import type { AwsClients, CoreOptions } from "./types"; +import { resourceNameFromArn } from "./arn"; import { toClientConfig } from "./utils"; export type PolicyGenerationWait = { @@ -32,10 +33,6 @@ export type PolicyGenerationWait = { const DEFAULT_WAIT: PolicyGenerationWait = { maxWaitTime: 60, minDelay: 2, maxDelay: 5 }; -function resourceIdFromArn(value: string): string { - return value.startsWith("arn:") ? value.slice(value.lastIndexOf("/") + 1) : value; -} - export class PolicyClient implements CorePolicyClient { constructor( private readonly clients: AwsClients, @@ -49,7 +46,7 @@ export class PolicyClient implements CorePolicyClient { signal?: AbortSignal, ): AsyncGenerator { const control = this.clients.control(toClientConfig(options)); - const gatewayId = resourceIdFromArn(input.gatewayId); + const gatewayId = resourceNameFromArn(input.gatewayId); yield { type: "step", message: `Resolving gateway ${gatewayId}` }; const gateway = await control.send(new GetGatewayCommand({ gatewayIdentifier: gatewayId }), { @@ -62,7 +59,7 @@ export class PolicyClient implements CorePolicyClient { `gateway '${gatewayId}' has no Policy Engine attached; pass --policy-engine-id`, ); } - const policyEngineId = resourceIdFromArn(engine); + const policyEngineId = resourceNameFromArn(engine); yield { type: "step", message: `Starting policy generation ${input.name}` }; const started = await control.send( diff --git a/src/core/project/bedrockAgentImport/baseTranslator.ts b/src/core/project/bedrockAgentImport/baseTranslator.ts index 7b108e3f8..3cb133edb 100644 --- a/src/core/project/bedrockAgentImport/baseTranslator.ts +++ b/src/core/project/bedrockAgentImport/baseTranslator.ts @@ -1,3 +1,4 @@ +import { regionFromArn, resourceNameFromArn } from "../../arn"; import { IMPORT_NOTES_FILE, renderImportNotes } from "./importNotes"; import { generateImportPyproject } from "./pyproject"; import type { @@ -220,7 +221,7 @@ def ${pythonIdentifier(fn.name)}(${parameters}) -> str: */ export function providerFromModelArn(foundationModel: string): string | undefined { if (!foundationModel.startsWith("arn:")) return undefined; - const resource = foundationModel.split("/").pop() ?? ""; + const resource = resourceNameFromArn(foundationModel); const provider = resource.split(".")[0]; return provider && provider !== resource ? provider.toLowerCase() : undefined; } @@ -230,7 +231,7 @@ export function providerFromModelArn(foundationModel: string): string | undefine * its ARN and fall back to the agent's region only when the ARN was unavailable. */ export function knowledgeBaseRegion(knowledgeBase: { arn?: string }, agentRegion: string): string { - return knowledgeBase.arn?.split(":")[3] || agentRegion; + return regionFromArn(knowledgeBase.arn ?? "") ?? agentRegion; } /** The root snapshot followed by every collaborator reachable from it, depth-first. */ diff --git a/src/core/project/templates/export.ts b/src/core/project/templates/export.ts index fb0c37d0e..d42c29e5b 100644 --- a/src/core/project/templates/export.ts +++ b/src/core/project/templates/export.ts @@ -19,6 +19,7 @@ import type { Memory } from "../../../projectSchemas/memory"; import type { EnvLocalEntry } from "../../../handlers/project/types"; import { InputValidationError } from "../../../errors/errors"; import { toPythonPackageName } from "../fsUtils"; +import { resourceNameFromArn } from "../../arn"; type ProjectSpec = z.infer; @@ -768,9 +769,7 @@ function resolveSkills( for (const skill of gitSkillSources) { const reference = skill.auth?.credentialArn ?? skill.auth?.credentialName; if (!reference) continue; - const name = reference.includes("/") - ? reference.slice(reference.lastIndexOf("/") + 1) - : reference; + const name = resourceNameFromArn(reference); if (seenGitCredentials.has(name)) continue; seenGitCredentials.add(name); if (!credentials.some((c) => c.name === name)) {