Skip to content

Dependency policies#1168

Draft
ericmj wants to merge 25 commits into
mainfrom
dependency-policies
Draft

Dependency policies#1168
ericmj wants to merge 25 commits into
mainfrom
dependency-policies

Conversation

@ericmj
Copy link
Copy Markdown
Member

@ericmj ericmj commented May 22, 2026

No description provided.

ericmj added 25 commits May 21, 2026 01:35
Adds mix_hex_pb_policy generated module and extends the vendoring
script to track hex_pb_policy. mix_hex_registry gains encode_policy/
decode_policy/build_policy/unpack_policy and mix_hex_repo gains
get_policy/2, mirroring the recently-added hex_core API.
Adds the :policy slot in Hex.State (env HEX_POLICY, config :policy,
default []). Hex.Policy.Sources parses the three accepted shapes —
keyword list, list of keyword lists, comma-separated string — and
deduplicates by (repo, name) preserving order. Hex.Repo gains
get_policy/2 wrapping the vendored :mix_hex_repo.get_policy/2.
Parallel HTTP fetch via Hex.Repo.get_policy, signature-verified
through the vendored mix_hex_repo. Decoded policies are stored on
disk as erlang terms with an ETag sidecar; on fetch failure the
last-known-good copy is loaded with a stale warning. Caches older
than 30 days hard-fail for that specific policy with a clear error.

Hex.Repo.get_policy/2 now derives repo_organization from the
"hexpm:<org>" key and strips the /repos/<org> URL suffix so the
vendored hex_repo can build the right policy URL and verify the
payload's repository field.

Adds two computed Hex.State slots, :policies and
:policy_filtered_versions, as stash points for later phases.
Hex.Policy.Filter.classify/3 evaluates a single policy against a
release (advisory severity threshold + retirement reason set);
classify_set/3 ANDs across the active set and records every blocker
for diagnostics.

Hex.Policy.Cooldown.strictest/2 combines local and policy cooldown
contributions and returns the longest duration string. source/2
identifies the contributor (`:local` or `{repo, name}`).
Hex.Policy.load_all reads the configured refs, dedups, and fetches
the active set with cache fallback. converge/2 stashes the result
under Hex.State up-front; on load failure Mix.raise surfaces a clear
error.

setup_cooldown now combines the local cooldown with policy cooldowns
via Hex.Policy.Cooldown.strictest/2 — the existing cooldown filter
handles age-based rejection for both sources.

The solver runs against Hex.Registry.Policy, which wraps
Hex.Registry.Cooldown: per-candidate classification via
Hex.Policy.Filter.classify_set/2, with denied candidates filtered
out and diagnostics recorded under :policy_filtered_versions.
Hex.Policy.Diagnostics owns rendering: resolution_summary/3,
preflight_error/4, failure_note/1, and format_load_error/1.

RemoteConverger.policy_preflight! runs after verify_input and before
the solver, scanning every direct request whose constraint has no
eligible version under the active policy set and raising a focused
preflight_error.

After every resolution that loaded a policy, print_policy_summary
prints the active set, the effective cooldown, and per-policy
hidden counts. On solver failure, a Note: block is appended listing
the responsible policies for transitive deps whose candidate set
was emptied.

normalize_advisories / severity_atom_to_int were lifted from
Hex.Registry.Policy into Hex.Policy.Filter so both the registry
wrapper and the pre-flight share one canonical conversion.
`mix hex.policy show` (default) prints the active policy set with
per-policy visibility, cooldown, advisory rule, retirement rule,
and the effective cooldown.

`mix hex.policy why PACKAGE` walks every version of PACKAGE in the
registry, classifies each against the active policies via
Hex.Policy.Filter, and prints a per-version status table.
`mix hex.policy why` now loads policies cold (same fallback as
`show`) and accepts a `REPO/PACKAGE` form so private-repo packages
can be inspected. Per-version blocker output names the reason
(advisory severity / retirement reason) instead of collapsing to
just the policy name.

Loader.read_cache tolerates a missing .etag sidecar instead of
crashing. Diagnostics.preflight_error no longer hardcodes a
`** (Mix) ` prefix — that's Mix.raise/1's job — so the rendered
error doesn't double up.
`mix help hex.policy` now shows the three opt-in mechanisms (mix.exs,
HEX_POLICY, mix hex.config) with examples, and points at the new
hex.pm docs page. The `mix hex.config` `policy` bullet clarifies the
CLI form (string) versus the mix.exs richer keyword form, so users
don't try to pass keyword lists through `mix hex.config KEY VALUE`.
Sources.load_all/0 reads policies from mix.exs, HEX_POLICY, and
~/.hex/hex.config independently and unions+dedups. The :policy
state slot is removed — Hex.Policy.Sources is now the source of
truth, matching the documented intersection-across-sources behavior.
Tests cover multi-source union explicitly.
The 304-conditional branch was unreachable — Hex.Repo.get_policy/2
never threaded an etag through build_hex_core_config/3 — and the
.etag sidecar was write-only. Removing the etag plumbing simplifies
the loader and tightens the cache state machine.
The 30-day staleness cap is about how long a network adversary can
suppress refreshes — cache file age, not policy publish time. Switch
days_old/1 to read File.stat mtime. The test now touches the cached
file backwards in time to exercise the stale-cap path.
Reject inputs like "myorg/" and "/foo" that produce empty repo or
package halves; surface a Mix.raise instead of carrying empty
strings into the registry.
The global hexpm repo carries no organization-scoped policies and
Hex.Repo.get_policy/2 would have nothing useful to do with a bare
"hexpm" ref. Reject "hexpm/<name>" in both string and keyword
configurations so the failure surfaces at config-parse time.
Picks up the upstream removal of Policy.published_at and the
contiguous renumbering (visibility=4, advisory_min_severity=5,
retirement_reasons=6, cooldown=7). Drops a now-meaningless comment
in the loader.
Resolution can succeed even when individual versions are blocked by an
active policy, so failing before the solver runs is wrong. Let the
solver explore the candidate set; Hex.Registry.Policy filters at
solve time and Diagnostics.failure_note attributes blocks on failure.
Drop the bespoke Hex.Policy.Loader and route policy fetches through
the same ETS-backed cache, Hex.Parallel runner, and etag/304 path
that handles /packages/<name>. Adds prefetch_policies/1 + policy/2
to Hex.Registry.Server, get_policy/3 etag arg to Hex.Repo, and
honors HEX_OFFLINE.
…icy.Filter

The filter carried two parallel representations of every enum: hex_core
decoded atoms from the registry, plus an int form invented to match the
threshold passed in by the policy resource. Converting the threshold to
its atom symbol once and ranking with Enum.find_index removes the
severity_to_int / retired_to_int / normalize_advisories helpers and lets
blockers describe themselves with the symbol the rest of the codebase
already speaks.
Hex.Policy.Cooldown duplicated duration parsing and source attribution
that already lived in Hex.Cooldown, and the remote converger held a
second copy of the bypass/locked-versions builders. Collapsing both into
Hex.Cooldown — strictest/1 over a tagged list, setup/2 as the single
entry point — drops a module and the converger's private helpers.
The diagnostics module shipped its own severity_label/retirement_label
clauses keyed on the int form that Hex.Policy.Filter no longer carries.
Delegating to Hex.Utils.advisory_severity/1 and package_retirement_reason/1
keeps blocker output consistent with mix hex.audit and removes the
parallel label table.
The hex.policy task carried its own active_policies_or_load helper that
read Hex.State, fell back to Hex.Policy.load_all, and cached the result.
That belongs next to the loader itself, so callers that need the active
set ask Hex.Policy for it directly instead of reimplementing the cache.
The task duplicated severity/retirement label tables that already live
in Hex.Utils, and rendered why output by ad-hoc string concatenation.
Going through the shared helpers and Mix.Tasks.Hex.print_table aligns
hex.policy with the rest of the CLI surface (matching hex.audit's
uppercase severity labels).
Hex.Policy.Sources ran its own project/env/global config plumbing in
parallel to Hex.State. Splitting the policy key into project / env /
global entries and adding a config_scope flag lets Hex.State own
parsing and source attribution, so Sources.load_all/0 is now just a
union over three state lookups.
The function carried its own \"hexpm:\" <> org parser and a suffix-strip
of the cached URL to recover the organization. default_organization/3
already builds the config map with everything else derived from the
parent repo, so stashing the organization name there at construction
time turns get_policy/3 into a simple Map.get lookup.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant