can1357/oh-my-pi · error · AwsCredentialsError
profile
profile
Error message
AWS profile role chain contains a cycle at '${profile}'. What it means
AIError.AwsCredentialsError with kind 'profile' thrown by resolveProfileChain in packages/ai/src/providers/aws-credentials.ts. When an AWS profile specifies role_arn + source_profile, the resolver follows the chain recursively, tracking visited profiles in a `seen` set; if it reaches a profile already in the set, the configuration is a role-assumption cycle (A assumes via B, B assumes via A) and it throws instead of looping forever.
Source
Thrown at packages/ai/src/providers/aws-credentials.ts:224
const configIni = loadSharedConfig ? await readIniFile(configPath) : undefined;
return resolveProfileChain(profile, { credentialsIni, configIni, region, signal, fetchImpl }, new Set());
}
/**
* Resolve one profile, following `role_arn` chains. A `role_arn` profile derives
* base credentials from `source_profile` (recursive), `web_identity_token_file`,
* or `credential_source`, then exchanges them via STS. Non-role profiles resolve
* directly from static keys, SSO, or `credential_process`. `seen` guards against
* `source_profile` cycles.
*/
async function resolveProfileChain(
profile: string,
ctx: ProfileResolveContext,
seen: Set<string>,
): Promise<ResolvedCredentials | undefined> {
if (seen.has(profile)) {
throw new AIError.AwsCredentialsError(`AWS profile role chain contains a cycle at '${profile}'.`, "profile");
}
seen.add(profile);
// Static credentials live in ~/.aws/credentials; SSO/role config lives in
// ~/.aws/config under `[profile foo]`. Merge into a single view.
const merged: Record<string, string> = {
...(ctx.configIni?.[profile] ?? {}),
...(ctx.credentialsIni?.[profile] ?? {}),
};
if (Object.keys(merged).length === 0) return undefined;
if (merged.role_arn) return assumeRoleFromProfile(profile, merged, ctx, seen);
if (merged.aws_access_key_id && merged.aws_secret_access_key) {
const out: ResolvedCredentials = {
accessKeyId: merged.aws_access_key_id,
secretAccessKey: merged.aws_secret_access_key,
};View on GitHub (pinned to 9690622007)
Solutions
- Open ~/.aws/config (and ~/.aws/credentials) and find the cycle: trace source_profile links starting from the profile in the error message
- Break the cycle by pointing the last link's source_profile at a profile with static credentials (no role_arn/source_profile)
- If two profiles must assume each other's roles, split them: keep one chain one-directional (base -> role1 -> role2)
- Rename the ambiguous profile if a copy/paste duplicate exists, then update AWS_PROFILE / caller references
Example fix
// before (~/.aws/config) [profile a] role_arn = arn:aws:iam::111:role/r1 source_profile = b [profile b] role_arn = arn:aws:iam::222:role/r2 source_profile = a // after: terminate chain at static creds [profile a] role_arn = arn:aws:iam::111:role/r1 source_profile = base [profile base] aws_access_key_id = AKIA... aws_secret_access_key = ...
Defensive patterns
Strategy: validation
Validate before calling
function assertNoProfileCycle(profiles: Record<string, { source_profile?: string }>, start: string): void {
const seen = new Set<string>();
let cur: string | undefined = start;
while (cur) {
if (seen.has(cur)) throw new Error(`AWS profile role chain contains a cycle at '${cur}'`);
seen.add(cur);
cur = profiles[cur]?.source_profile;
}
} Try / catch
try {
const result = await session.prompt(bedrockModel, messages);
} catch (err) {
if (err instanceof AIError.AwsCredentialsError && err.kind === "profile") {
console.error(`Fix ~/.aws/config: ${err.message}`);
return;
}
throw err;
} Prevention
- Before committing ~/.aws/config changes, trace each source_profile link to ensure the chain ends at a static-credential profile
- Never set a profile's source_profile to itself or to a profile that transitively points back to it
- Use distinct base profiles for each role chain instead of cross-referencing role profiles
- Run `aws sts get-caller-identity --profile <name>` after config edits to validate the chain resolves
When it happens
Trigger: ~/.aws/config defines profiles where source_profile (or the chain via role_arn/source_profile and assumeRoleFromProfile) references back to an ancestor: e.g. [profile a] role_arn=... source_profile=b and [profile b] role_arn=... source_profile=a; resolving credentials with AWS_PROFILE=a (or readProfileCredentials/assumeRoleFromProfile on it) hits the cycle.
Common situations: Copy-pasting profile blocks and leaving source_profile pointing at the wrong (circular) profile; renaming profiles so a previously linear chain accidentally loops; generated config from tooling that cross-references two roles; typo where source_profile equals the profile itself.
Related errors
- Unable to resolve AWS credentials. Configure static environm
- ${source} response has a missing or invalid Expiration.
- Antigravity credentials missing projectId
- Google Cloud credentials missing projectId
- imageUrls exposure "named-cloudflared" requires credentials.
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/c7c73505e70762ac.
Report an issue: GitHub.