fix: apm install no longer adopts files not produced by installed plugins (closes #1199)#1256
fix: apm install no longer adopts files not produced by installed plugins (closes #1199)#1256sergio-sisternes-epam wants to merge 3 commits intomainfrom
Conversation
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…1199) The audit function _check_unmanaged_files already correctly distinguishes managed vs unmanaged files. Add 5 regression tests to lock this behaviour and prevent future regressions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Adds unit-level regression coverage around the unmanaged-files policy check for governance directories, and updates the changelog to mention the #1199 fix intent.
Changes:
- Add new unit tests asserting hand-rolled files under
.github/instructions/,.github/hooks/, and.github/agents/are reported as unmanaged unless present in lockfile provenance (includinglocal_deployed_files). - Add a unit test asserting a managed directory-prefix in one governance directory (e.g.
.github/skills/.../) does not mask unmanaged files in another (e.g..github/instructions/). - Add a
CHANGELOG.mdentry under Fixed referencing the #1199 regression area.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| tests/unit/policy/test_policy_checks.py | Adds regression tests for unmanaged-files auditing across multiple governance directories and lockfile provenance sources. |
| CHANGELOG.md | Adds an Unreleased “Fixed” entry referencing the unmanaged-files hardening / #1199 area. |
APM Review Panel:
|
| Persona | B | R | N | Takeaway |
|---|---|---|---|---|
| Python Architect | 0 | 1 | 1 | Regression tests are structurally sound and exercise real code paths, but the install-path fix that caused #1199 is absent -- the audit was already correct; the bug is in check_collision / LockfilePhase. |
| CLI Logging Expert | 0 | 0 | 1 | Test detail assertions match the implementation output contract; no CLI output regressions introduced. One pre-existing nit: detail lines omit an actionable hint. |
| DevX UX Expert | 0 | 1 | 1 | Tests protect the audit correctness contract; user-facing error message names files but gives no remediation hint. Minor gap worth a follow-up. |
| Supply Chain Security Expert | 0 | 2 | 1 | Tests-only PR; implementation already correct. Two concerns: fail-open scan-cap is now regression-pinned as intended behavior, and the security-critical audit surface lacks integration-with-fixtures coverage. |
| OSS Growth Hacker | 0 | 0 | 1 | Trust fix with a solid story angle; CHANGELOG entry slightly undersells the user promise -- one word swap would make it shareable. |
| Doc Writer | 0 | 1 | 2 | CHANGELOG entry misattributes the fix as a test addition and cites the PR number instead of the closed issue (#1199); recommend rewording to describe the behavioral fix. |
| Test Coverage Expert | 0 | 0 | 1 | Five well-targeted regression-trap unit tests for #1199; unit tier is adequate for this pure-function surface; no blocking gaps. S7 probe skipped (pytest unavailable in runner). |
B = blocking-severity findings, R = recommended, N = nits.
Counts are signal strength, not gates. The maintainer ships.
Top 5 follow-ups
- [Python Architect] Add the src/ install-pipeline fix: guard
BaseIntegrator.check_collisionwhenmanaged_files is None, and/or intersectLockfilePhase._attach_deployed_filesagainstctx.written_filesbefore committingdeployed_files. -- The root cause of [bug] apm install integrates files in .github/instructions/ that are not produced by an installed plugin #1199 --apm installsilently adopting pre-existing unmanaged governance files -- is unresolved. The CHANGELOG entry and PR title claim the issue is closed; it is not. - [Supply Chain Security Expert] Open a follow-up issue to change the scan-cap path from fail-open (
result.passed=True) to fail-warn or fail-closed before this regression pin is merged. -- Adversarial file creation can silence the unmanaged-files audit entirely by exceeding_MAX_UNMANAGED_SCAN_FILES. Pinning the current behavior as expected makes the cap harder to fix later; this is agoverned-by-policy/secure-by-defaultsurface. - [Supply Chain Security Expert] Add an integration-with-fixtures test: run
apm installagainst a temp repo containing a hand-rolled.github/hooks/file not produced by any plugin and assert non-zero exit. --.github/hooks/is a code-execution surface; unit-only coverage does not verify the audit fires at the CLI boundary where the bug manifests. - [Doc Writer] Rewrite the CHANGELOG entry to describe the behavioral fix (not the tests), cite
closes #1199instead of#1256, and use user-facing language per the oss-growth-hacker suggestion. -- The Fixed section must answer 'what changed for users'; the current entry describes test evidence and will confuse readers scanning release notes. - [DevX UX Expert] Append a remediation hint to each unmanaged-file detail line in the
CheckResultoutput (pre-existing gap, now regression-pinned by new tests). -- Package-manager UX contract: an actionable diagnostic names the next command. Users seeing a bare path list with no suggested fix will open support issues instead of self-resolving.
Architecture
classDiagram
direction LR
class UnmanagedFilesPolicy {
<<ValueObject>>
+action str
+directories list[str]
}
class LockFile {
<<Repository>>
+dependencies dict[str, LockedDependency]
+local_deployed_files list[str]
+read(path) LockFile
+write(path) None
}
class LockedDependency {
<<ValueObject>>
+deployed_files list[str]
+deployed_file_hashes dict
}
class _check_unmanaged_files {
<<Pure>>
+call(project_root, lock, policy) CheckResult
}
class CheckResult {
<<ValueObject>>
+name str
+passed bool
+message str
+details list[str]
}
class BaseIntegrator {
<<AbstractBase>>
+check_collision(target_path, rel_path, managed_files, force) bool
+validate_deploy_path(rel_path) bool
}
class InstructionIntegrator {
<<ConcreteIntegrator>>
+integrate_instructions_for_target(target, pkg_info, project_root) IntegrationResult
}
class LockfilePhase {
<<CommandPhase>>
+_attach_deployed_files(lockfile) None
}
class TestUnmanagedFilesPolicy {
<<TestClass>>
+test_handrolled_instruction_flagged_as_unmanaged()
+test_handrolled_file_not_masked_by_deployed_deps()
+test_handrolled_file_across_governance_dirs()
+test_local_deployed_files_not_flagged()
+test_dir_prefix_does_not_mask_instructions()
}
LockFile *-- LockedDependency : contains
_check_unmanaged_files ..> LockFile : reads deployed_files
_check_unmanaged_files ..> UnmanagedFilesPolicy : reads config
_check_unmanaged_files ..> CheckResult : returns
InstructionIntegrator --|> BaseIntegrator
LockfilePhase ..> InstructionIntegrator : calls integrate
LockfilePhase ..> LockFile : writes deployed_files
TestUnmanagedFilesPolicy ..> _check_unmanaged_files : calls directly
TestUnmanagedFilesPolicy ..> LockFile : constructs fixture
class TestUnmanagedFilesPolicy:::touched
classDef touched fill:#fff3b0,stroke:#d47600
flowchart TD
A(["apm install CLI entry"])
B["integrate_instructions_for_target\ninstruction_integrator.py:60"]
C{"BaseIntegrator.check_collision\nbase_integrator.py:61\nmanaged_files is None?"}
D["returns False\nBUG: pre-existing file not skipped"]
E["Copy file to governance dir\n.github/instructions/*.md"]
F["Append to deployed_files"]
G["LockfilePhase._attach_deployed_files\ninstall/phases/lockfile.py:126\nwrites deployed_files to lockfile"]
H["apm.lock.yaml written\npre-existing file now appears managed"]
I(["apm audit / policy check"])
J["_check_unmanaged_files\npolicy_checks.py:685\nrglob governance dirs"]
K{"rel in deployed set?"}
L["File appears managed\nWRONG: was hand-rolled"]
M["File flagged as unmanaged\nCORRECT"]
N["TestUnmanagedFilesPolicy\n5 new regression tests\ncalls _check_unmanaged_files directly\nwith hand-crafted LockFile fixtures"]
O["Tests PASS: audit function correct\nBug path in install NOT covered"]
A --> B
B --> C
C -->|"managed_files is None"| D
D --> E
E --> F
F --> G
G --> H
H --> I
I --> J
J --> K
K -->|"yes - pre-existing file was added"| L
K -->|"no"| M
N -.->|"exercises"| J
N -.->|"result"| O
Recommendation
Hold for the src/ install-pipeline fix. The five regression tests are a net positive and should be preserved, but merging this PR as-is would close #1199 in the tracker while the root cause remains live in production -- a silent breaking promise to users and a trust risk for the 'Governed by policy' differentiator. The highest-leverage next action for the author is to add the BaseIntegrator.check_collision / LockfilePhase._attach_deployed_files guard (python-architect's diagnosis), update the CHANGELOG entry to user-facing language citing closes #1199, and open a separate issue for the scan-cap fail-open path before or concurrent with merge. The test additions can land as-is once those three items are addressed.
Full per-persona findings
Python Architect
-
[recommended] Install-path provenance fix is missing; tests exercise the already-correct audit, not the defective code path at
src/apm_cli/integration/base_integrator.py:82
The PR body states 'check file provenance against the declared plugin manifests' as the approach. Tracing the actual bug:BaseIntegrator.check_collisionreturnsFalsewhenmanaged_files is None, so pre-existing files in governance dirs ARE deployed and appear indep.deployed_files, making_check_unmanaged_filesreport them as managed. The five new tests call_check_unmanaged_filesdirectly with hand-craftedLockFilefixtures -- they prove the audit function is correct (which it was before this PR) but do not cover the install pipeline path (integrate_instructions_for_target->LockfilePhase._attach_deployed_files) where the provenance leak occurs.
Suggested: The provenance fix belongs inintegrate_instructions_for_target: before appendingtarget_pathtodeployed_files, assert the file was written by this install run. Alternatively, teachLockfilePhase._attach_deployed_filesto intersectdep_filesagainstctx.written_filesrather than all files returned by the integrator. Tests for the fix belong intests/unit/install/ortests/integration/.
Proof (missing at integration-with-fixtures):tests/unit/install/-- proves: apm install does not mark pre-existing, unmanaged governance files as deployed when managed_files is None [governed-by-policy,secure-by-default] -
[nit] CHANGELOG entry may contain a stale editorial placeholder at
CHANGELOG.md
The added CHANGELOG line may contain author reminder text rather than release prose.
CLI Logging Expert
- [nit] Unmanaged-file detail lines surface a bare path but no remediation hint at
src/apm_cli/policy/policy_checks.py:768
TheCheckResult.detailslist contains bare posix paths iterated verbatim inaudit.pyandpolicy_gate.py. Pre-existing gap, not introduced by this PR, but the new regression tests cement the current output contract, making a future improvement slightly harder to slip in quietly.
Suggested: Add a trailing action hint to each detail line, e.g.unmanaged.append(rel + ' # add to a managed package or exclude in apm.yml policy'). The new tests would need updating too, but that is a small cost for better UX.
DevX UX Expert
-
[recommended] Unmanaged-files diagnostic lists file paths but offers no remediation hint at
src/apm_cli/policy/policy_checks.py:767
Whenapm auditflags unmanaged files the user sees only a count + path list with no hint about which plugin to install or suggestedapm installinvocation. By the package-manager mental model an actionable error always names the next command.
Suggested: Append a one-line remediation hint to theCheckResultdetails: 'Runapm install <plugin>to adopt a file, or add it tolocal_deployed_filesin apm.lock.yaml to mark it as locally managed.'
Proof (missing at unit):tests/unit/policy/test_policy_checks.py-- proves: When unmanaged files are detected the diagnostic message includes a concrete remediation step, not just a file list. [devx,governed-by-policy] -
[nit] Scan-cap silently passes the check instead of warning the user at
src/apm_cli/policy/policy_checks.py:734
When file scan exceeds_MAX_UNMANAGED_SCAN_FILESthe check returnspassed=True. CI sees a green check and never knows governance directories were too large to scan.
Suggested: Escalate the message to stderr or use a distinctstatus='skipped'field so CI surfaces can surface it.
Supply Chain Security Expert
-
[recommended] Scan-cap fail-open path is regression-pinned as correct behavior, making the bypass permanent at
tests/unit/policy/test_policy_checks.py:631
test_rglob_cap_skips_checkassertsresult.passed=Truewhen file count exceeds_MAX_UNMANAGED_SCAN_FILES. An adversary with write access to a governance directory can create N+1 files to trigger the cap and silence the unmanaged-files audit entirely. Pinning this as expected outcome raises the cost of ever making the cap fail-closed.
Suggested: Change the cap behavior to emit a blocking result with a 'scan-capped' detail that CI can gate on.
Proof (passed at unit):tests/unit/policy/test_policy_checks.py::TestUnmanagedFiles::test_rglob_cap_skips_check-- proves: The scan-cap path silently passes the security check when governance directories are large, with no blocking signal. [secure-by-default,governed-by-policy] -
[recommended] Unmanaged-files audit for
.github/hooks/has no integration-with-fixtures coverage
The.github/hooks/governance directory is a code-execution surface. All 5 regression tests are unit-tier (tmp_path, no realapm installsubprocess). No test verifies the audit fires during an actualapm installinvocation against a fixture repo.
Suggested: Add an integration-with-fixtures test that runsapm installagainst a temp repo containing a hand-rolled.github/hooks/file absent from any dependency'sdeployed_files, and asserts the command exits non-zero.
Proof (missing at integration-with-fixtures):tests/unit/policy/test_policy_checks.py-- proves: The unmanaged-files audit reliably blocks install when .github/hooks/ contains a file not deployed by any plugin. [secure-by-default,governed-by-policy] -
[nit]
test_local_deployed_files_not_flaggeddoes not assert the negative case for_SELF_KEYexemption attests/unit/policy/test_policy_checks.py:745
Only the happy path is tested. A negative test -- listing a path NOT on disk -- would confirm the exemption is not a blanket allowlist.
OSS Growth Hacker
- [nit] CHANGELOG entry frames this as a test hardening story, not a trust guarantee for end users at
CHANGELOG.md
The current line reads 'regression tests prove hand-rolled files...are correctly flagged' -- this is maintainer language. Users care that the audit they rely on is trustworthy, not that tests exist.
Suggested: 'Fix: unmanaged-files audit now correctly flags hand-rolled governance files that were silently treated as managed -- preventing false-clean audit results (closes [bug] apm install integrates files in .github/instructions/ that are not produced by an installed plugin #1199).'
Auth Expert -- inactive
PR only touches CHANGELOG.md and tests/unit/policy/test_policy_checks.py -- no auth, token management, credential resolution, or host classification surfaces are affected.
Doc Writer
-
[recommended] CHANGELOG entry describes the regression tests, not the fix -- violates 'what changed for users' contract of the Fixed section at
CHANGELOG.md
The Fixed section answers 'what behavior changed'. The current entry -- 'regression tests prove hand-rolled files in governance directories are correctly flagged' -- describes evidence, not the fix. Users scanning the changelog cannot tell thatapm installpreviously adopted unmanaged files.
Suggested:- Fix unmanaged-files audit regression: apm install no longer adopts files in governance directories that were not produced by installed plugins; regression tests added to prevent recurrence. (closes #1199) -
[nit] Entry cites PR fix: apm install no longer adopts files not produced by installed plugins (closes #1199) #1256 but the fix closes issue [bug] apm install integrates files in .github/instructions/ that are not produced by an installed plugin #1199 -- cross-reference should point at the issue at
CHANGELOG.md
Every other Fixed entry cites the issue number (closes #NNN). Readers following#1256land on the PR, not the original bug report.
Suggested: Replace(#1256)with(closes #1199)at the end of the entry. -
[nit] Policy docs page worth a 30-second author check to confirm
unmanaged_filesbehavior description is accurate post-fix atdocs/src/content/docs/enterprise/policy-reference.md
If the docs described the intended (correct) behavior all along, no change is needed. But if they implied any tolerance for hand-rolled files being adopted, a one-sentence clarification would close the gap.
Test Coverage Expert
- [nit] S7 run-probe skipped: pytest unavailable in runner; test correctness assessed by code read only
Five regression tests inTestUnmanagedFilesPolicycover the [bug] apm install integrates files in .github/instructions/ that are not produced by an installed plugin #1199 scenarios correctly. The implementation inpolicy_checks.pylooks correct. However, pytest was not available in the runner environment, so test outcomes areunknownrather than confirmed. All 5 tests look structurally correct and should pass against the current implementation.
Proof (unknown at unit):tests/unit/policy/test_policy_checks.py-- proves: Five regression tests for [bug] apm install integrates files in .github/instructions/ that are not produced by an installed plugin #1199 cover instruction flagging, sibling masking, cross-directory detection, local_deployed_files exemption, and trailing-slash prefix scoping. [governed-by-policy]
This panel is advisory. It does not block merge. Re-apply the
panel-review label after addressing feedback to re-run.
Generated by PR Review Panel for issue #1256 · ● 4.4M · ◷
…1199) check_collision() previously returned False (no collision) when managed_files was None, silently bypassing collision detection. Now None is treated as an empty set so pre-existing governance files are always protected from silent overwrite. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Fixes #1199
Problem
apm installintegrates files in.github/instructions/that pre-exist but are not produced by any installed plugin. This causes theunmanaged-filesaudit to report clean when it should flag them.Approach
During integration, check file provenance against the declared plugin manifests. Only files produced by installed plugins should be treated as managed; pre-existing files without provenance should be left untouched.
Draft PR -- implementation in progress.