Skip to content

Enforce apm-policy.yml at apm install time, not only in apm audit --ci #827

@danielmeppiel

Description

@danielmeppiel

Summary

apm install does not enforce apm-policy.yml. Today, organisational policy is only enforced by apm audit --ci. A developer running apm install on their workstation can pull and integrate dependencies that the org policy explicitly denies, with no warning and no failure.

The discovery, parsing, and enforcement primitives all already exist in src/apm_cli/policy/ — they just aren't wired into the install pipeline.

Evidence

  • src/apm_cli/commands/install.py — no references to discover_policy, run_policy_checks, or ApmPolicy.
  • src/apm_cli/install/phases/resolve.py — same: zero policy hooks. The only policy-adjacent code (install/insecure_policy.py) handles the unrelated HTTP-dependency consent prompt.
  • src/apm_cli/commands/audit.py:507-540 — the only call site of discover_policy() + run_policy_checks(). Honours enforcement: block | warn | off.
  • src/apm_cli/policy/policy_checks.py:702-760run_policy_checks() already evaluates dependency allowlist, denylist, required packages, required version, etc. against apm.yml + lockfile.

Impact

The current model relies on apm audit --ci being a required CI check on every PR. That assumption is brittle:

  1. Local-only workflows bypass the gate entirely. Developers running apm install against a private branch, a fork, or a personal scratch repo never hit CI, so banned packages install silently.
  2. Transitive dependencies bypass the gate at install time. A policy-allowed direct dep can pull a denied transitive dep; the user only learns at audit time, after the file system already has the integrated content (and .github/ files have been deployed).
  3. The integration step has already happened by the time CI fails. Files have been written to .github/prompts/, .github/skills/, etc. CI tells you "this should not have been installed" but the developer's working tree is already polluted.
  4. Mismatch with developer mental model. Coming from npm, pip, cargo, users reasonably expect the package manager itself to enforce org policy at install time, not a separate audit command.

Proposed approach (high level)

  1. Call discover_policy(project_root) early in install/phases/resolve.py, after apm.yml parse and after the dependency graph is resolved (so transitive deps are also evaluable).
  2. Run the dependency-scoped subset of run_policy_checks (_check_dependency_allowlist, _check_dependency_denylist, _check_required_package_version, plus the MCP equivalents) against the resolved set.
  3. Map enforcement levels:
    • block -> fail the install before any download or file integration.
    • warn -> print a deferred-diagnostics summary and continue.
    • off -> skip entirely.
  4. Honour an escape hatch (e.g. --no-policy flag plus an APM_POLICY=off env var) for offline / break-glass scenarios. Log loudly when used.
  5. Use the existing policy cache (apm_modules/.policy-cache/) so the install path is not network-bound on every invocation.
  6. Apply the gate uniformly to: apm install, apm install <pkg>, apm install --mcp, and apm update. (Currently only audit --ci gates these.)

Open questions

  • Default-deny on policy fetch failure? If the org policy URL is unreachable (offline, network error), do we fail closed or fall back to permissive? Audit today errors out (commands/audit.py:526); install probably wants the same behaviour when a project has previously seen a policy (cache hit), and a configurable behaviour when no cache exists.
  • apm install --no-cache behaviour? Should it also bust the policy cache, or keep policy cached separately to avoid surprise enforcement changes mid-install?
  • First-run UX. When a developer clones a repo for the first time and runs apm install, the policy fetch is the very first network call. We should make sure errors here are clearly distinguished from dependency-download errors.
  • Where does the gate live architecturally? Most natural: a new phase between resolve and download (e.g. install/phases/policy_gate.py), composed into the existing pipeline (install/pipeline.py). Mirrors the _check_insecure_dependencies pattern that already runs at the end of resolve.
  • Telemetry / diagnostics surface. Use DiagnosticCollector + CommandLogger.error() so blocked installs render a grouped, scannable summary consistent with the rest of apm install output.

Out of scope for this issue

  • Changes to the policy schema itself (policy/schema.py) — the dependencies field is already sufficient.
  • Changes to apm audit --ci behaviour — it stays the source of truth for CI gating; install enforcement is additive.
  • Discovery model changes (auto-discovery via <org>/.github/apm-policy.yml) — already correct, just needs to be invoked.

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementDeprecated: use type/feature. Kept for issue history; will be removed in milestone 0.10.0.policyDeprecated: use area/audit-policy. Kept for issue history; will be removed in milestone 0.10.0.securityDeprecated: use theme/security. Kept for issue history; will be removed in milestone 0.10.0.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions