Skip to content
Draft
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
64 changes: 64 additions & 0 deletions policy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Hex organization dependency policy

A `Policy` is a signed resource published by an organization that the Hex client honors at resolution time to filter the candidate set of package releases. Policies are optional and opt-in client-side; the registry server distributes them but does not enforce them.

## Resource location

`/repos/REPO/policies/NAME` on the same backend that serves `/packages/NAME` and the other registry resources.

`NAME` matches `^[a-z0-9][a-z0-9_\-\.]*[a-z0-9]$`, length 3..64, and is unique within the repository.

## Encoding

The payload is the [`Policy`](/registry/policy.proto) protobuf message, wrapped in a [`Signed`](/registry/signed.proto) envelope (RSA-SHA512 signature against the payload), gzipped.

The signing key is the repository's signing key — the same key used to sign `/names`, `/versions`, and `/packages/NAME`.

## Visibility

The `visibility` field controls who can fetch the resource:

* `VISIBILITY_PRIVATE` — the resource is served only to authenticated callers who can access the repository. Same auth pipeline as `/packages/NAME` on a private repository.
* `VISIBILITY_PUBLIC` — the resource is served to any caller, authenticated or not, so projects that are not members of the repository can opt in to the policy.

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

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.

### Advisory rule

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` (`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.

### Retirement rule

If `retirement_reasons` is non-empty, the policy blocks any release whose `retired.reason` field is one of the listed values. Reasons map 1:1 to `RetirementReason` in `package.proto` (`RETIRED_OTHER=0` … `RETIRED_RENAMED=4`).

### Cooldown rule

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.

When multiple active policies declare cooldowns, the effective cooldown is the strictest. Local cooldown configuration cannot lower it.

Unlike the advisory and retirement rules, which compose by intersection across active policies, multiple cooldowns compose by taking the strictest (longest) duration.

## Client behavior

A conformant client:

1. **Reads policy references from multiple opt-in sources** (e.g., project file, environment variable, global config) and composes them (intersection): a release must pass every active policy. The active set is deduplicated on `(repository, name)`.
2. **Fetches and verifies each active policy** before resolution. Signature verification uses the configured public key for the repository.
3. **Filters the candidate set at resolution time only.** Lockfile entries are trusted at install; filtering does not apply to versions 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 — it bounds the suppression window for a network adversary.

## Cross-references

* [`registry/policy.proto`](/registry/policy.proto) — protobuf schema.
* [`registry/package.proto`](/registry/package.proto) — `AdvisorySeverity` and `RetirementReason` enums.
* [`registry-v2.md`](/registry-v2.md) — registry resources index.
* [Hex dependency cooldown spec](https://gist.github.com/ericmj/16488f164ca2045e12f0f79a73c45031) (draft proposal; the duration grammar and resolution-time filtering model are shared).
3 changes: 3 additions & 0 deletions registry-v2.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ The following files hold information about the packages in the repository.
* `/packages/NAME`
* This file exists for every package in the repository, it contains all the releases of that package and all dependencies of the releases.
* Encoded using protobuf schema [`Package`](/registry/package.proto).
* `/repos/REPO/policies/NAME`
* This file may exist for any organization repository, it contains a signed dependency policy that opted-in clients honor at resolution time. See [`policy.md`](/policy.md).
* Encoded using protobuf schema [`Policy`](/registry/policy.proto).

All registry files are compressed using `gzip`.

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

message Policy {
// Name of repository
required string repository = 1;

// Policy name within the repository
// (matches ^[a-z0-9][a-z0-9_\-\.]*[a-z0-9]$, length 3..64)
required string name = 2;

// Optional, free-form description (admin-set, surfaced in CLI/UI)
optional string description = 3;

// Whether the policy is publicly readable or restricted to org members.
// Read at the edge to decide whether to enforce auth on the fetch.
// Adding new Visibility values is a breaking change — old clients will
// 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;
}

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