{"record":{"id":"23e02bdc470f8d9f","repo":"zeroclaw-labs/zeroclaw","slug":"iam-policy-duplicate-role-mapping-for-normalized","errorCode":null,"errorMessage":"IAM policy: duplicate role mapping for normalized key '{}' (from nevis_role '{}') — remove or merge the duplicate entry","messagePattern":"IAM policy: duplicate role mapping for normalized key '(.+?)' \\(from nevis_role '(.+?)'\\) — remove or merge the duplicate entry","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-runtime/src/security/iam_policy.rs","lineNumber":93,"sourceCode":"                .zeroclaw_permissions\n                .iter()\n                .filter(|p| !p.eq_ignore_ascii_case(\"all\"))\n                .map(|p| p.trim().to_ascii_lowercase())\n                .collect();\n\n            let all_workspaces = mapping\n                .workspace_access\n                .iter()\n                .any(|w| w.eq_ignore_ascii_case(\"all\"));\n            let allowed_workspaces: Vec<String> = mapping\n                .workspace_access\n                .iter()\n                .filter(|w| !w.eq_ignore_ascii_case(\"all\"))\n                .map(|w| w.trim().to_ascii_lowercase())\n                .collect();\n\n            if role_map.contains_key(&key) {\n                bail!(\n                    \"IAM policy: duplicate role mapping for normalized key '{}' \\\n                     (from nevis_role '{}') — remove or merge the duplicate entry\",\n                    key,\n                    mapping.nevis_role\n                );\n            }\n\n            role_map.insert(\n                key,\n                CompiledRole {\n                    all_tools,\n                    allowed_tools,\n                    all_workspaces,\n                    allowed_workspaces,\n                },\n            );\n        }\n","sourceCodeStart":75,"sourceCodeEnd":111,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-runtime/src/security/iam_policy.rs#L75-L111","documentation":"IamPolicy::from_mappings found two role mappings whose nevis_role values collide after normalization (trim + ASCII lowercase). Duplicates are rejected instead of silently applying last-wins because an accidental overwrite could broaden or revoke access in the deny-by-default policy engine.","triggerScenarios":"Config lists the same role with different casing (\"Admin\" and \"admin\"), or the same role in two files/sections merged at load; YAML anchors or includes duplicating a role mapping block; refactors that copied a block and edited only permissions.","commonSituations":"Multi-file config merges where casing drifts; hand-edited configs adding a role that already exists upstream; CI environments that concatenate config fragments.","solutions":["Find the two entries named in the message (the normalized key and the offending nevis_role) and merge their permissions/workspaces into one.","Add a config-lint step that fails on case-insensitive duplicate role keys before deploy.","Adopt one casing convention (e.g. always lowercase) for nevis_role values across all sources.","If roles truly differ, rename one of them in Nevis so normalized keys are distinct."],"exampleFix":"# before\n[[iam.role_mappings]]\nnevis_role = \"Admin\"\nzeroclaw_permissions = [\"all\"]\n\n[[iam.role_mappings]]\nnevis_role = \"admin\"\nzeroclaw_permissions = [\"fs_read\"]\n\n# after — one merged entry\n[[iam.role_mappings]]\nnevis_role = \"Admin\"\nzeroclaw_permissions = [\"all\"]","handlingStrategy":"validation","validationCode":"fn duplicate_role_key(ms: &[RoleMapping]) -> Option<String> {\n    let mut seen = std::collections::HashSet::new();\n    for m in ms {\n        let k = m.nevis_role.trim().to_ascii_lowercase();\n        if !k.is_empty() && !seen.insert(k) {\n            return Some(k);\n        }\n    }\n    None\n}","typeGuard":null,"tryCatchPattern":"if let Some(key) = duplicate_role_key(&mappings) {\n    anyhow::bail!(\"config invalid: duplicate role '{key}' — merge before deploy\");\n}\nlet policy = IamPolicy::from_mappings(&mappings)?;","preventionTips":["Lint IAM config in CI for case-insensitive duplicate role keys.","Adopt one casing convention for role names across all config sources.","When merging config fragments, run a uniqueness check on normalized keys before loading."],"tags":["iam","config","duplicate-key","policy","authorization"],"backgroundTag":"duplicate-config-key","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}