{"record":{"id":"b7b8378fa115183d","repo":"openai/codex","slug":"permissions-profile-inheritance-cycle-detected","errorCode":null,"errorMessage":"permissions profile inheritance cycle detected: {}","messagePattern":"permissions profile inheritance cycle detected: (.+?)","errorType":"validation","errorClass":"PermissionProfileResolutionError","httpStatus":null,"severity":"error","filePath":"codex-rs/config/src/permissions_toml.rs","lineNumber":139,"sourceCode":"#[derive(Debug, Clone, PartialEq, Eq, Error)]\npub enum PermissionProfileResolutionError {\n    #[error(\"default_permissions refers to undefined profile `{profile_name}`\")]\n    UndefinedProfile { profile_name: String },\n    #[error(\n        \"permissions profile `{profile_name}` extends undefined profile `{parent_profile_name}`\"\n    )]\n    UndefinedParent {\n        profile_name: String,\n        parent_profile_name: String,\n    },\n    #[error(\n        \"permissions profile `{profile_name}` cannot extend unsupported built-in profile `{parent_profile_name}`\"\n    )]\n    UnsupportedBuiltInParent {\n        profile_name: String,\n        parent_profile_name: String,\n    },\n    #[error(\n        \"permissions profile inheritance cycle detected: {}\",\n        cycle.join(\" -> \")\n    )]\n    Cycle { cycle: Vec<String> },\n    #[error(\"failed to serialize permissions profile while resolving inheritance: {source}\")]\n    SerializeProfileToml {\n        #[source]\n        source: toml::ser::Error,\n    },\n    #[error(\n        \"failed to deserialize merged permissions profile while resolving inheritance: {source}\"\n    )]\n    DeserializeProfileToml {\n        #[source]\n        source: toml::de::Error,\n    },\n}\n","sourceCodeStart":121,"sourceCodeEnd":157,"githubUrl":"https://github.com/openai/codex/blob/339751715c64496cb86246bfb3935f40e309dd3d/codex-rs/config/src/permissions_toml.rs#L121-L157","documentation":"resolve_profile records each profile name it visits while following extends; when the next name already appears in the chain it stops and returns Cycle, with the loop rendered as `a -> b -> a`. Permission inheritance must form a DAG that terminates at a profile without extends; both self-extension and multi-hop loops are rejected before any merging happens.","triggerScenarios":"`[permissions.a] extends = \"b\"` together with `[permissions.b] extends = \"a\"`; or a profile whose extends equals its own name. Resolving any profile on the loop fails as soon as the loop closes.","commonSituations":"Two profiles refactored to share settings and accidentally cross-linked; copy-pasted extends lines; long chains where an edit reintroduces an earlier name; generated configs that splice overrides cyclically.","solutions":["Follow the exact cycle printed in the message and remove or repoint the extends that closes it (usually the last link)","Factor shared settings into one base profile that others extend — a tree, not a ring","Re-run profile resolution after each extends edit; cycles are detected before merge, so no partial state is written"],"exampleFix":"# before — a -> b -> a\n[permissions.a]\nextends = \"b\"\n[permissions.b]\nextends = \"a\"\n\n# after — one base, one child\n[permissions.base]\nnetwork = { enabled = false }\n[permissions.a]\nextends = \"base\"","handlingStrategy":"validation","validationCode":"use std::collections::BTreeMap;\nuse codex_config::PermissionProfileToml;\n\n// DFS with a visited set over extends edges before calling resolve_profile.\nfn find_inheritance_cycle(\n    entries: &BTreeMap<String, PermissionProfileToml>,\n    start: &str,\n) -> Option<Vec<String>> {\n    let mut seen: Vec<String> = Vec::new();\n    let mut name = start.to_string();\n    loop {\n        if let Some(pos) = seen.iter().position(|n| n == &name) {\n            return Some(seen[pos..].to_vec());\n        }\n        seen.push(name.clone());\n        let profile = entries.get(&name)?; // None => chain ends, no cycle\n        name = profile.extends.clone()?;   // None => chain ends, no cycle\n    }\n}","typeGuard":"// True when the profile's own name does not appear in its extends target\n// (catches the direct self-loop; use the DFS above for full chains).\nfn extends_itself(name: &str, profile: &PermissionProfileToml) -> bool {\n    profile.extends.as_deref() == Some(name)\n}","tryCatchPattern":"match permissions.resolve_profile(name, builtin_lookup) {\n    Err(PermissionProfileResolutionError::Cycle { cycle }) => {\n        // cycle is e.g. [\"a\", \"b\", \"a\"]; remove or repoint the extends that\n        // closes the loop (usually the last link) and retry.\n    }\n    Err(e) => return Err(e.into()),\n    Ok(profile) => { /* ... */ }\n}","preventionTips":["Model inheritance as a tree: shared settings go in one base profile that others extend","A profile's extends must never name an ancestor — or itself","Re-run profile resolution in CI config checks so cycles are caught before deploy"],"tags":["permissions","config","profile","inheritance","cycle","rust"],"backgroundTag":"config-inheritance-cycle","analyzedSha":"339751715c64496cb86246bfb3935f40e309dd3d","analyzedAt":"2026-08-25T05:35:09.876Z","schemaVersion":2},"datasetVersion":"2026-08-25T06:17:31.827Z"}