Skip to content

feat(policy): add dependencies.require_pinned_constraint to ban unbounded ranges #1491

@danielmeppiel

Description

@danielmeppiel

Summary

Add a new boolean field policy.dependencies.require_pinned_constraint
to apm.policy.yml. When set to true, apm install (and apm audit)
flags any dependency whose constraint is unbounded — *, a bare
>=X.Y.Z without an upper bound, or a bare branch ref — and routes
through the existing enforcement: warn | block | off setting.

Motivation

Governed installs rely on lockfiles to deliver reproducibility, but
the apm.yml manifest itself can express intent that defeats lockfile
discipline on the next apm update. A dep written as:

dependencies:
  apm:
    - acme-org/some-skills        # no ref - tracks default branch
    - other-org/lib@>=1.0         # no upper bound - any 99.x is valid
    - third-org/pkg#*             # any version

Will resolve to whatever exists today, but the next apm update may
pull a major-version jump or a moving branch tip. Enterprise audit
flows want to declare "we only ship deps that pin to a bounded
constraint." That is what this policy field expresses.

Proposal

Add a single additive field on DependencyPolicy:

@dataclass(frozen=True)
class DependencyPolicy:
    allow: tuple[str, ...] | None = None
    deny: tuple[str, ...] | None = None
    require: tuple[str, ...] | None = None
    require_resolution: str = "project-wins"
    max_depth: int = 50
    require_pinned_constraint: bool = False    # NEW

YAML:

# apm.policy.yml
policy:
  enforcement: block          # existing field, governs how violations surface
  dependencies:
    require_pinned_constraint: true

A dep's constraint is considered "unbounded" if any of the following
hold:

  1. The ref is missing / empty (resolves to default branch).
  2. The ref is a bare branch name (anything that does not parse as a
    semver range and is not a SHA or literal tag).
  3. The semver range is *, x, X, or a single-sided lower bound
    without a paired upper bound (e.g. >=1.0.0 alone).
  4. The constraint is a SHA-resolved branch alias under a name that
    moves (we keep this case as a follow-up — a SHA is technically
    pinned even if it came from a branch).

Conversely, the following count as pinned:

The check runs in the policy preflight phase (alongside
run_dependency_policy_checks) on the declared constraints, not
the resolved ones, so the policy is meaningful before the install
completes.

Examples

With require_pinned_constraint: true and enforcement: block:

# apm.yml
dependencies:
  apm:
    - acme-org/skills              # FAIL: no ref
    - other/lib@>=1.0              # FAIL: unbounded upper
    - third/lib#^1.2.0             # OK: caret range
    - fourth/lib@v1.5.3            # OK: literal tag

Output:

[x] Policy violation: 2 dependencies use unbounded constraints.
    - acme-org/skills          (no ref; resolves to default branch)
    - other/lib@>=1.0          (unbounded upper; pair with '<2.0' or use '^1.0')
    Hint: pin to a semver range, literal tag, or SHA.
    Enforcement: block. Install aborted.

With enforcement: warn, the same lines render with [!] and install
proceeds.

Considerations

Out of scope

  • A separate allowlist of "approved unbounded patterns" (e.g. allow
    main on internal repos). If demand surfaces, follow up with
    require_pinned_constraint: {except: [...]}.
  • Enforcement of registry-pinned hashes (registry deps already pin
    via resolved_hash in the lockfile, per feat(registry): [FEATURE] Add package registry support for dependency… #1471).
  • Migrating the existing require_resolution semantics.

References

  • PR feat(registry): [FEATURE] Add package registry support for dependency… #1471 added RegistrySourcePolicy alongside DependencyPolicy
    in src/apm_cli/policy/schema.py.
  • Existing call sites for dependency policy checks:
    policy_gate, policy_target_check, run_policy_checks,
    run_policy_preflight.
  • Inspiration: npm config set save-exact true, pip's
    --require-hashes, cargo workspaces' resolver lockstep.

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    area/audit-policyapm-policy.yml schema, custom_checks, install-time enforcement.status/needs-triageNew, awaiting maintainer review.theme/governanceGoverned by policy. apm-policy, audit, enforcement, enterprise rollout.type/featureNew capability, new flag, new primitive.

    Type

    No type
    No fields configured for issues without a type.

    Projects

    Status

    Done

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions