Dependency policies#1168
Merged
Merged
Conversation
6e7e434 to
9c58eb5
Compare
Pull in the restructured Policy resource: a Policy now carries a repeated
RepositoryPolicy ("tab") per repository it constrains, each with an optional
baseline Restriction (advisory_min_severity, retirement_reasons, cooldown) and
a list of allow/deny Overrides. Restriction imports package.proto and types
advisory_min_severity and retirement_reasons as the AdvisorySeverity and
RetirementReason enums, so decoded values are symbolic.
Introduce organization-defined dependency policies that filter the versions
available during resolution. A policy is fetched through the repository like
the registry (etag/304, last-known-good fallback, HEX_OFFLINE).
A policy configures one or more repositories ("tabs", typically the org's own
repo and "hexpm"); a repository with no tab is unconstrained by the policy.
Each tab has an optional baseline restriction (advisory severity, retirement
reason, release-age cooldown) applied to every release in the repository, plus
allow/deny overrides as the per-package lever — an allow override permits a
release and exempts it from the restriction, a deny override blocks it, and the
most specific matching override wins. Versions already in the lockfile are
exempt from filtering.
- Hex.Policy and Hex.Policy.{Sources,Filter,Diagnostics}: config plumbing via
Hex.State, AND-composition of policy sources, per-repository classification
(overrides then restriction), and resolution diagnostics.
- Hex.Registry.Policy / Hex.Registry.Server: policies are prefetched and cached
alongside the registry; locked versions bypass policy filtering.
- Cooldown: the local cooldown drives the global cutoff; policy cooldowns are
per-repository restrictions enforced in Hex.Policy.Filter, so AND composition
yields the strictest minimum age per repository. The effective cooldown is
surfaced in diagnostics and mix hex.policy.
- mix hex.policy inspects active policies.
b55fc8f to
ef15376
Compare
A project opts into exactly one policy, so the three separate config sources (project/env/global) collapse into one ordinary `policy` config key with the usual override precedence (HEX_POLICY, then mix.exs, then hex.config), matching every other Hex setting. This removes the multi-source union/dedup/ordering machinery: the `:policies` map becomes a single `:active_policy`, `Hex.Policy.Sources` is deleted (its parsing folds into `Hex.Policy`), and the filter and registry stop handling lists of policies.
Fail closed instead of open on enforcement gaps: - A malformed policy config (HEX_POLICY, mix.exs, or hex.config) now aborts resolution instead of degrading to unenforced behind a warning. Hex.State keeps the invalid value tagged and Hex.Policy.load/0 raises. - A configured policy that fails to materialize from the registry raises instead of being silently dropped. - Unknown advisory severities (future enum values or missing fields) rank above every known severity so they can never slip under a threshold. - Policy fetches for an org repo whose custom URL does not follow the /repos/<org> layout return an error instead of deriving a wrong URL. Registry server: - close/persist now wait for in-flight policy fetches; previously a queued close could delete the ETS table mid-fetch and crash the server. - Update the fun2ms comment for the policy matchspec entries and simplify prefetch_policies_offline. Diagnostics and UX: - Print the resolution summary's effective cooldown line only when the policy is the source; the local cooldown already has its own filtered-versions report. - Singular forms for "1 candidate version" / "1 version of". - mix hex.policy raises the conventional short invalid-arguments message instead of dumping the moduledoc. - Cooldown.strictest/1 normalizes unparseable durations to 0d. - Align the cooldown source format between the resolution summary and mix hex.policy show, expose print_policy_summary for testing, and update stale multi-policy docs in mix hex.config.
- Record a blocked version only once even when the solver asks for a package's versions repeatedly, so summary counts are accurate - An empty HEX_POLICY disables the configured policy for the invocation - Reject bare `mix hex.policy`; the show subcommand must be explicit - Accept [org: "ORG", name: "NAME"] in the mix.exs policy config, expanding to the hexpm:<org> repo, and correct the REPO/NAME terminology in the invalid-config error and task docs - List hidden versions per package in the policy summary and failure note, newest-first and capped at 5 with a pointer to `mix hex.policy why`; apply the same shape to the cooldown report
Hex.Policy.parse_config now validates and normalizes to the canonical REPO/NAME string instead of a parsed tuple, with the ref split moved to the point of use in Hex.Policy.load. This lets the policy key flow through mix hex.config's generic read/list/set/delete paths like every other config key, removing all special-casing and a bug where the listing printed the policy twice.
maennchen
reviewed
Jun 16, 2026
| @@ -0,0 +1,210 @@ | |||
| defmodule Hex.Policy.Filter do | |||
Member
There was a problem hiding this comment.
Should this be a helper module in hex_core?
Member
Author
There was a problem hiding this comment.
Yes, we can move there it in the future.
ericmj
commented
Jun 16, 2026
Co-authored-by: Jonatan Männchen <jonatan@maennchen.ch> Co-authored-by: Eric Meadows-Jönsson <eric.meadows.jonsson@gmail.com>
The applied review suggestions did not compile (invalid `:hex_pb_policy.Policy()`
typespecs and a `ref:%{` spacing error) and, once made to parse, replaced
`Map.get(map, :key, default)` with direct field access and `%{key: nil}`
patterns. hex_core decodes protobuf with maps_unset_optional => omitted, so
unset optional fields (restriction, overrides, repositories) are absent from
the decoded map rather than nil, and the direct access crashed. Restore the
defensive Map.get-based version.
Hex.Repo.get_policy peeled the /repos/<org> suffix off the repo URL so hex_core could re-insert it via a separate repo_organization config field. Re-vendor hex_core, where get_policy now builds its URL from repo_url like every other resource and verifies the payload against repo_name, and drop the now-unneeded put_repo_organization translation. A custom repo URL no longer has to end in /repos/<org> to carry a policy.
a963b79 to
2033548
Compare
maennchen
approved these changes
Jun 16, 2026
Document that rejecting the bare hexpm repo is deliberate and would be relaxed if a global policy is ever served directly under hexpm.
maennchen
approved these changes
Jun 16, 2026
maennchen
added a commit
to hexpm/specifications
that referenced
this pull request
Jun 17, 2026
Reflect Hex (Elixir/Mix) client support for consuming security advisories and enforcing organization-defined dependency policies (cooldown, advisory severity, retirement, overrides) at resolution time. Rebar3 and Gleam do not yet consume advisories or enforce policies. See hexpm/hexpm#1622 and hexpm/hex#1168. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Resolve the vendored hex_core conflicts by re-vendoring from hex_core main (d6a6a5a), which already carries both the policy proto and the new hex_auth/hex_cli_auth modules. The textual merge of lib/hex/repo.ex left the organization config key inconsistent: main standardized on :repo_organization (base URL + build_url inserting /repos/<org>), so drop the policy branch's leftover :organization key and update the policy registry test to main's convention.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds client-side enforcement of dependency policies. A policy is defined on hex.pm under an organization (or on any self-hosted repo) and fetched through the registry at resolution time. The client filters candidate versions the policy blocks before the solver sees them, so resolution simply never picks them.
Per repository, a policy can express:
Versions already in the lockfile are exempt from filtering, so re-resolution keeps a locked-but-now-blocked entry instead of failing. Policies have a visibility: public policies are fetchable anonymously, private ones require authentication to the owning organization.
Configuring the policy
A project has exactly one active policy, resolved with the usual Hex config precedence: env var, then
mix.exs, then global config.HEX_POLICYenv var, aREPO/NAMEpair. Overrides everything for the invocation, and an empty value disables the configured policy:mix.exs:hexblock, withorg:for a hexpm organization orrepo:for any configured repo:mix hex.config, stored in the global Hex config:The bare
hexpmrepo is rejected in all forms: the global repository has no organization-scoped policies, so policies live underhexpm:<org>or a self-hosted repo.New mix tasks
mix hex.policy showsummarizes the active policy: its visibility, the per-repository restrictions (cooldown, advisory rule, retirement rule) and override counts, plus the effective cooldown, which is the strictest of the localcooldownconfig and the policy's own.mix hex.policy why PACKAGE(orREPO/PACKAGE) walks every version of the package in the registry, classifies each against the active policy, and prints a per-version table:This is the uncapped companion to the resolution output below.
mix hex.config policyreads and writes the setting like any other config key.How policies surface in mix deps.get
After a successful resolution, a summary block reports the active policy, the cooldown it imposes (a locally configured cooldown keeps its own existing report), and every candidate version it hid, capped at the 5 newest per package:
When resolution fails and the policy hid versions of the packages involved, the solver's failure message is followed by a note attributing the hidden versions, in the same capped format, so a "no compatible versions" error explains itself:
Because a policy is an enforcement feature, anything short of materializing it fails closed rather than resolving unenforced:
Invalid policy configuration: ...and the expected formatsFailed to fetch policy REPO/NAME from registryand aborts, unless a previously fetched copy is cached, in which case the cache is used and the message says somix hex.user authwhen no authenticated user is present