From 0eaf73ad4878af880cf0536b74673e089f4738da Mon Sep 17 00:00:00 2001 From: Hweinstock Date: Wed, 9 Sep 2026 14:24:45 +0000 Subject: [PATCH 1/5] refactor(core): decode IAM role ARNs via shared arn helpers --- src/core/abTestExecutionRole.tsx | 6 +++--- src/core/executionRole.tsx | 3 ++- src/core/onlineEvalExecutionRole.tsx | 5 +++-- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/core/abTestExecutionRole.tsx b/src/core/abTestExecutionRole.tsx index 721f4d026..0c5969901 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"; @@ -17,12 +18,11 @@ export function abTestExecutionRoleName(testName: string): string { } export function roleNameFromArn(roleArn: string): string { - const parts = roleArn.split("/"); - return parts[parts.length - 1] ?? roleArn; + return resourceNameFromArn(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; } 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..926db9fcc 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 @@ -50,7 +51,7 @@ function truncatedRolePrefix(configName: string): string { } export function roleNameFromArn(roleArn: string): string { - return roleArn.slice(roleArn.lastIndexOf("/") + 1); + return resourceNameFromArn(roleArn); } // isManagedOnlineEvalRole recognises the CLI's default role for a config. Roles @@ -188,7 +189,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}"`); } From 215bc3f4c43c219f77f206336e667d122a45ed29 Mon Sep 17 00:00:00 2001 From: Hweinstock Date: Wed, 9 Sep 2026 14:24:46 +0000 Subject: [PATCH 2/5] refactor(core): use resourceNameFromArn for policy gateway ids --- src/core/policy.tsx | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) 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( From 2c508872289ba7b7b90e9b176beb49a99c7a9f66 Mon Sep 17 00:00:00 2001 From: Hweinstock Date: Wed, 9 Sep 2026 14:24:46 +0000 Subject: [PATCH 3/5] refactor(import): decode model and knowledge-base ARNs via arn helpers --- src/core/project/bedrockAgentImport/baseTranslator.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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. */ From 31c88f3156957c02326f25cca64c2699c4ecbda6 Mon Sep 17 00:00:00 2001 From: Hweinstock Date: Wed, 9 Sep 2026 16:03:42 +0000 Subject: [PATCH 4/5] refactor(core): drop roleNameFromArn wrappers for direct resourceNameFromArn --- src/core/abTestExecutionRole.tsx | 6 +----- src/core/eval.tsx | 6 +++--- src/core/onlineEvalExecutionRole.tsx | 6 +----- 3 files changed, 5 insertions(+), 13 deletions(-) diff --git a/src/core/abTestExecutionRole.tsx b/src/core/abTestExecutionRole.tsx index 0c5969901..458db3a78 100644 --- a/src/core/abTestExecutionRole.tsx +++ b/src/core/abTestExecutionRole.tsx @@ -17,10 +17,6 @@ export function abTestExecutionRoleName(testName: string): string { return `${base.slice(0, 55)}-${hash}`; } -export function roleNameFromArn(roleArn: string): string { - return resourceNameFromArn(roleArn); -} - export function accountIdFromArn(arn: string): string { const accountId = parseArn(arn)?.account; if (!accountId) throw new Error(`could not extract account id from ARN: ${arn}`); @@ -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/onlineEvalExecutionRole.tsx b/src/core/onlineEvalExecutionRole.tsx index 926db9fcc..e02a6f40a 100644 --- a/src/core/onlineEvalExecutionRole.tsx +++ b/src/core/onlineEvalExecutionRole.tsx @@ -50,15 +50,11 @@ function truncatedRolePrefix(configName: string): string { return `${ROLE_NAME_PREFIX}${configName.slice(0, room)}-`; } -export function roleNameFromArn(roleArn: string): string { - return resourceNameFromArn(roleArn); -} - // 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)); From 9d7e3eb12f8660a49e993e63bf020c934a6f631b Mon Sep 17 00:00:00 2001 From: Hweinstock Date: Wed, 9 Sep 2026 16:09:02 +0000 Subject: [PATCH 5/5] refactor(export): use resourceNameFromArn for git-skill credential name --- src/core/project/templates/export.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) 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)) {