Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 13 additions & 6 deletions registry-v2.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,15 +102,22 @@ The `visibility` field controls who can fetch the resource:

The auth decision is made per-object by inspecting the payload's `visibility` field; the path and signing model are identical in both cases. If the payload cannot be decoded — signature mismatch, unknown enum value, missing required field — the edge must fail closed and require authentication.

### Rule semantics
### Repository policies

Each policy declares zero or more categorical rules and an optional cooldown. A release is blocked by the policy if **any** of its declared rules blocks the release.
A policy carries a list of [`RepositoryPolicy`](/registry/policy.proto) entries, one per repository it constrains — in practice `hexpm` (public packages) and the organization's own repository. Each candidate release is matched to the entry whose `repository` equals the release's repository. A release from a repository with no matching entry is unconstrained by the policy.

If `advisory_min_severity` is set, the policy blocks any release whose maximum advisory severity is greater than or equal to `advisory_min_severity`. Severities map 1:1 to `AdvisorySeverity` in [`package.proto`](/registry/package.proto) (`SEVERITY_NONE=0` … `SEVERITY_CRITICAL=4`). Setting this to `0` (`SEVERITY_NONE`) is permitted and blocks any release that has any advisory at all, regardless of declared severity.
A matched entry has two parts, evaluated in this order for each candidate release `{repository, package, version}`:

If `retirement_reasons` is non-empty, the policy blocks any release whose `retired.reason` is one of the listed values. Reasons map 1:1 to `RetirementReason` in [`package.proto`](/registry/package.proto) (`RETIRED_OTHER=0` … `RETIRED_RENAMED=4`).
1. **Overrides** (`overrides`) — the final say. An `OVERRIDE_ACTION_ALLOW` override whose `ref` matches the release permits it immediately and **bypasses the restriction**; an `OVERRIDE_ACTION_DENY` override blocks it. When several overrides match, the one with the most specific `requirement` wins (a `requirement`-bearing entry is more specific than a bare-`package` entry).
2. **Restriction** (`restriction`) — applied to every release in the repository, but **never** to a release permitted by an `ALLOW` override. A release is blocked if any limit fires.

If `cooldown` is set and non-zero, the policy blocks any release whose `published_at` is more recent than `now - cooldown_duration`. The grammar matches the Hex cooldown configuration grammar: `"Nd"`, `"Nw"`, `"Nmo"`, or `"0"`; unset or `"0"` disables the rule.
A `PackageRef` (used by `Override.ref`) matches a release when its `package` equals the release's package and, if `requirement` is set, the release's version satisfies that requirement.

#### Restriction limits

* `advisory_min_severity` is set and the release's maximum advisory severity is greater than or equal to it. It is an `AdvisorySeverity` (imported from [`package.proto`](/registry/package.proto), `SEVERITY_NONE` … `SEVERITY_CRITICAL`). `SEVERITY_NONE` blocks any release that has any advisory at all.
* `retirement_reasons` is non-empty and the release's `retired.reason` is one of the listed values. Each is a `RetirementReason` (imported from [`package.proto`](/registry/package.proto), `RETIRED_OTHER` … `RETIRED_RENAMED`).
* `cooldown` is set and non-zero and the release's `published_at` is more recent than `now - cooldown_duration`. The grammar matches the Hex cooldown configuration grammar: `"Nd"`, `"Nw"`, `"Nmo"`, or `"0"`; `"0"` (or unset) imposes no minimum age. If multiple active policies declare cooldowns, the effective cooldown is the strictest one.

### Client behavior

Expand All @@ -121,7 +128,7 @@ A conformant client:
3. **Filters the candidate set at resolution time only.** Lockfile entries are trusted at install; filtering does not apply to versions already in the lockfile.
4. **Caches each policy independently** with last-known-good fall-back on fetch failure (network, 5xx, signature mismatch). The maximum staleness window should be at most 30 days, bounding the suppression window for a network adversary.

The advisory and retirement rules compose across active policies by intersection — any active policy can block a release. Cooldowns compose differently: the effective cooldown is the strictest (longest) duration across all active policies, and local cooldown configuration cannot lower it.
Across the active set, policies compose by intersection: a release survives only if every active policy permits it and no active policy's restriction blocks it. Cooldowns compose by strictest-wins — the effective cooldown is the longest duration across all active policies, and local cooldown configuration cannot lower it.

## Links

Expand Down
77 changes: 61 additions & 16 deletions registry/policy.proto
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
syntax = "proto2";

import "package.proto";

message Policy {
// Name of repository
required string repository = 1;
Expand All @@ -17,26 +19,69 @@ message Policy {
// treat unknown values as PRIVATE per the fail-closed rule.
required Visibility visibility = 4;

// Categorical advisory rule. If set, deny any release whose maximum
// advisory severity is at least this value. Values map to AdvisorySeverity
// in package.proto (SEVERITY_NONE..SEVERITY_CRITICAL = 0..4).
// Unset = rule disabled.
optional uint32 advisory_min_severity = 5;

// Categorical retirement rule. If non-empty, deny any release retired with
// a reason in this set. Values map to RetirementReason in package.proto
// (RETIRED_OTHER..RETIRED_RENAMED = 0..4). Empty = rule disabled.
repeated uint32 retirement_reasons = 6 [packed=true];

// Optional minimum release age for every package version governed by this
// policy. Same duration grammar as the Hex cooldown config ("7d", "2w",
// "1mo", "0"). Unset or "0" means no policy cooldown. If multiple active
// policies declare cooldowns, the effective cooldown is the strictest one.
optional string cooldown = 7;
// One entry per repository the policy constrains (in practice "hexpm" and
// the org's own repository). A candidate release is matched to the entry
// whose repository equals the release's repository; a release from a
// repository with no matching entry is unconstrained by this policy.
repeated RepositoryPolicy repositories = 5;
}

enum Visibility {
// PRIVATE is the safe default; unknown enum values must be treated as PRIVATE.
VISIBILITY_PRIVATE = 0;
VISIBILITY_PUBLIC = 1;
}

message RepositoryPolicy {
// Repository this entry applies to (e.g. "hexpm" or the org's repository).
required string repository = 1;

// Baseline limits applied to every release in this repository. Unset = no
// restriction. Restrictions never apply to releases permitted by an ALLOW
// override (those bypass all limits).
optional Restriction restriction = 2;

// Per-package final say, evaluated against each release in this repository.
// An ALLOW override permits the release immediately and bypasses
// `restriction`; a DENY override blocks it. When multiple overrides match a
// release, the one with the most specific requirement wins.
repeated Override overrides = 3;
}

message Restriction {
// Advisory limit. If set, deny any release whose maximum advisory severity
// is at least this value. Unset = no advisory limit.
optional AdvisorySeverity advisory_min_severity = 1;

// Retirement limit. If non-empty, deny any release retired with a reason in
// this set. Empty = no retirement limit.
repeated RetirementReason retirement_reasons = 2 [packed=true];

// Minimum release age. Same duration grammar as the Hex cooldown config
// ("7d", "2w", "1mo", "0"). Unset or "0" = no minimum age. If multiple
// active policies declare cooldowns, the effective cooldown is the strictest.
optional string cooldown = 3;
}

message PackageRef {
// Package name.
required string package = 1;

// Optional version requirement (e.g. "~> 1.7"). Unset = the whole package.
optional string requirement = 2;
}

message Override {
// Whether this override permits or blocks the matching release.
required OverrideAction action = 1;

// The package (and optional requirement) the override applies to.
required PackageRef ref = 2;
}

enum OverrideAction {
// Permit the release and bypass `restriction`.
OVERRIDE_ACTION_ALLOW = 0;
// Block the release.
OVERRIDE_ACTION_DENY = 1;
}