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

  1. Open ~/.aws/config (and ~/.aws/credentials) and find the cycle: trace source_profile links starting from the profile in the error message
  2. Break the cycle by pointing the last link's source_profile at a profile with static credentials (no role_arn/source_profile)
  3. If two profiles must assume each other's roles, split them: keep one chain one-directional (base -> role1 -> role2)
  4. 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

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


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/c7c73505e70762ac. Report an issue: GitHub.