Summary
When a child policy declares extends: org but does NOT redeclare an unmanaged_files: block, the merged policy's unmanaged_files.action is silently downgraded to the model's default (ignore) instead of inheriting the parent's deny.
The _escalate(_UNMANAGED_ACTION_LEVELS, parent.action, child.action) call at src/apm_cli/policy/inheritance.py:198 correctly takes the max, but the child.unmanaged_files for a child that omits the block is not "no opinion" — it's UnmanagedFilesPolicy(action='ignore', directories=()). So max('deny', 'ignore') == 'deny' should hold but the resulting merged check reports as disabled.
Repro
org/.github/apm-policy.yml:
name: org
version: "1.0.0"
enforcement: block
unmanaged_files:
action: deny
directories:
- .github/instructions
- .github/agents
- .github/hooks
repo/apm-policy.yml:
name: child
version: "1.0.0"
extends: org
enforcement: block
dependencies:
deny:
- "**/some-pattern"
# (no unmanaged_files block)
Add an unmanaged file (e.g. .github/hooks/sneaky.hook.md) and run:
apm audit --ci --policy ./apm-policy.yml
Expected: unmanaged-files check fails with action deny inherited from org.
Actual: [+] unmanaged-files | Unmanaged files check disabled (action: ignore) and the audit passes.
Workaround
Redeclare the unmanaged_files block in every override that should preserve the parent's posture:
extends: org
unmanaged_files:
action: deny
directories:
- .github/agents
- .github/instructions
- .github/hooks
This loses the value proposition of extends: (single source of truth at the org root) — the override has to mirror the parent's directory list verbatim, which is exactly what the inheritance feature is supposed to avoid.
Suggested fix
Two routes:
- Make the model field on
UnmanagedFilesPolicy distinguish "default" from "explicitly set". A child that did NOT declare unmanaged_files: should be treated as transparent in _merge_unmanaged_files, returning parent unchanged. (Mirrors how _intersect_allow treats None as "no opinion".)
- Or: at parse time, propagate parent values into any child block fields the child did not explicitly set, before invoking the merge.
The first is cleaner because it matches the rest of the inheritance.py contract.
Affected versions
Reproduced with apm v0.x (current main). Same logic at src/apm_cli/policy/inheritance.py:194-200.
Impact
Org-wide governance posture silently weakens whenever a team adds a repo override for an unrelated policy area (e.g. extra dependency deny patterns). Hard to detect — apm audit --policy org shows the right floor, but apm audit --policy ./apm-policy.yml (which the consumer typically runs in CI for layered enforcement) silently ignores the floor for unmanaged_files.
Summary
When a child policy declares
extends: orgbut does NOT redeclare anunmanaged_files:block, the merged policy'sunmanaged_files.actionis silently downgraded to the model's default (ignore) instead of inheriting the parent'sdeny.The
_escalate(_UNMANAGED_ACTION_LEVELS, parent.action, child.action)call atsrc/apm_cli/policy/inheritance.py:198correctly takes the max, but thechild.unmanaged_filesfor a child that omits the block is not "no opinion" — it'sUnmanagedFilesPolicy(action='ignore', directories=()). Somax('deny', 'ignore') == 'deny'should hold but the resulting merged check reports as disabled.Repro
org/.github/apm-policy.yml:repo/apm-policy.yml:Add an unmanaged file (e.g.
.github/hooks/sneaky.hook.md) and run:Expected:
unmanaged-filescheck fails with actiondenyinherited from org.Actual:
[+] unmanaged-files | Unmanaged files check disabled (action: ignore)and the audit passes.Workaround
Redeclare the
unmanaged_filesblock in every override that should preserve the parent's posture:This loses the value proposition of
extends:(single source of truth at the org root) — the override has to mirror the parent's directory list verbatim, which is exactly what the inheritance feature is supposed to avoid.Suggested fix
Two routes:
UnmanagedFilesPolicydistinguish "default" from "explicitly set". A child that did NOT declareunmanaged_files:should be treated as transparent in_merge_unmanaged_files, returningparentunchanged. (Mirrors how_intersect_allowtreatsNoneas "no opinion".)The first is cleaner because it matches the rest of the inheritance.py contract.
Affected versions
Reproduced with
apmv0.x (current main). Same logic atsrc/apm_cli/policy/inheritance.py:194-200.Impact
Org-wide governance posture silently weakens whenever a team adds a repo override for an unrelated policy area (e.g. extra dependency deny patterns). Hard to detect —
apm audit --policy orgshows the right floor, butapm audit --policy ./apm-policy.yml(which the consumer typically runs in CI for layered enforcement) silently ignores the floor forunmanaged_files.