From 8a9455ced78bb489dba092df8df3f572109e61cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eric=20Meadows-J=C3=B6nsson?= Date: Thu, 21 May 2026 01:10:33 +0200 Subject: [PATCH 1/7] Add Policy registry resource specification Defines /repos//policies/ as a new signed registry resource with a Policy protobuf message carrying advisory, retirement, and cooldown rules. Includes a Visibility enum so the edge can decide auth gating per-object by decoding the payload. --- policy.md | 60 +++++++++++++++++++++++++++++++++++++++++++ registry-v2.md | 3 +++ registry/policy.proto | 44 +++++++++++++++++++++++++++++++ 3 files changed, 107 insertions(+) create mode 100644 policy.md create mode 100644 registry/policy.proto diff --git a/policy.md b/policy.md new file mode 100644 index 0000000..bef66dd --- /dev/null +++ b/policy.md @@ -0,0 +1,60 @@ +# 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//policies/` on the same backend that serves `/packages/` and the other registry resources. + +`` matches `[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, served with `Content-Type: application/octet-stream` and `Content-Encoding: gzip`. + +The signing key is the repository's existing signing key — the same key already used to sign `/names`, `/versions`, and `/packages/`. No new key infrastructure. + +## Visibility + +The `visibility` field controls who can fetch the resource: + +- `VISIBILITY_PRIVATE` — the resource is served only to authenticated callers who can already access the repository. Same auth pipeline as `/packages/` 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 that has at least one advisory whose 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`). + +### 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. + +## 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 with AND semantics: 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 already-locked versions. +4. **Caches each policy independently** with last-known-good fall-back on fetch failure (network, 5xx, signature mismatch). A maximum staleness window — recommended 30 days — 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) — sibling spec; the duration grammar and resolution-time filtering model are shared. diff --git a/registry-v2.md b/registry-v2.md index 2471720..b4aeec1 100644 --- a/registry-v2.md +++ b/registry-v2.md @@ -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//policies/` + * 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`. diff --git a/registry/policy.proto b/registry/policy.proto new file mode 100644 index 0000000..1bb298e --- /dev/null +++ b/registry/policy.proto @@ -0,0 +1,44 @@ +syntax = "proto2"; + +message Policy { + // Organization (repository) name + required string repository = 1; + + // Policy name within the repository (matches [a-z0-9_\-\.]+) + required string name = 2; + + // Optional, free-form description (admin-set, surfaced in CLI/UI) + optional string description = 3; + + // Resource generation timestamp, seconds since unix epoch. + // Clients use this to compute staleness when a fetch fails and the + // last-known-good cached payload is loaded instead. + required int64 published_at = 4; + + // Whether the policy is publicly readable or restricted to org members. + // Read at the edge (Fastly compute) to decide whether to enforce auth + // on the fetch. + required Visibility visibility = 5; + + // 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 = 6; + + // 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 = 7 [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 = 8; +} + +enum Visibility { + VISIBILITY_PRIVATE = 0; + VISIBILITY_PUBLIC = 1; +} From 453c2e9042bf9b27abf1c68f0539ef8c33c0b8c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eric=20Meadows-J=C3=B6nsson?= Date: Thu, 21 May 2026 01:17:11 +0200 Subject: [PATCH 2/7] Address spec review on Policy resource Unify placeholder casing (REPO/NAME), align advisory wording across proto and doc, document advisory_min_severity=0 semantics, replace "AND semantics" with "(intersection)", lift "recommended" to a normative SHOULD, drop vendor names from proto comments, mark the gist link as a draft proposal, and tighten name grammar to disallow leading/trailing non-alphanumerics. --- policy.md | 30 ++++++++++++++++-------------- registry-v2.md | 2 +- registry/policy.proto | 10 ++++++---- 3 files changed, 23 insertions(+), 19 deletions(-) diff --git a/policy.md b/policy.md index bef66dd..ad8e011 100644 --- a/policy.md +++ b/policy.md @@ -1,25 +1,25 @@ -# Hex Organization Dependency Policy +# 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//policies/` on the same backend that serves `/packages/` and the other registry resources. +`/repos/REPO/policies/NAME` on the same backend that serves `/packages/NAME` and the other registry resources. -`` matches `[a-z0-9_\-\.]+`, length 3..64, and is unique within the repository. +`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, served with `Content-Type: application/octet-stream` and `Content-Encoding: gzip`. -The signing key is the repository's existing signing key — the same key already used to sign `/names`, `/versions`, and `/packages/`. No new key infrastructure. +The signing key is the repository's existing signing key — the same key already used to sign `/names`, `/versions`, and `/packages/NAME`. No new key infrastructure. ## Visibility The `visibility` field controls who can fetch the resource: -- `VISIBILITY_PRIVATE` — the resource is served only to authenticated callers who can already access the repository. Same auth pipeline as `/packages/` 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. +* `VISIBILITY_PRIVATE` — the resource is served only to authenticated callers who can already 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. @@ -31,7 +31,9 @@ Each policy declares zero or more categorical rules and an optional cooldown. A ### Advisory rule -If `advisory_min_severity` is set, the policy blocks any release that has at least one advisory whose 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`). +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 @@ -39,7 +41,7 @@ If `retirement_reasons` is non-empty, the policy blocks any release whose `retir ### 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. +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. @@ -47,14 +49,14 @@ When multiple active policies declare cooldowns, the effective cooldown is the s A conformant client: -1. **Reads policy references from multiple opt-in sources** (e.g., project file, environment variable, global config) and composes them with AND semantics: a release must pass every active policy. The active set is deduplicated on `(repository, name)`. +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 already-locked versions. -4. **Caches each policy independently** with last-known-good fall-back on fetch failure (network, 5xx, signature mismatch). A maximum staleness window — recommended 30 days — bounds the suppression window for a network adversary. +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) — sibling spec; the duration grammar and resolution-time filtering model are shared. +* [`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). diff --git a/registry-v2.md b/registry-v2.md index b4aeec1..dde431c 100644 --- a/registry-v2.md +++ b/registry-v2.md @@ -17,7 +17,7 @@ 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//policies/` +* `/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). diff --git a/registry/policy.proto b/registry/policy.proto index 1bb298e..c92bcf9 100644 --- a/registry/policy.proto +++ b/registry/policy.proto @@ -1,7 +1,7 @@ syntax = "proto2"; message Policy { - // Organization (repository) name + // Name of repository required string repository = 1; // Policy name within the repository (matches [a-z0-9_\-\.]+) @@ -16,8 +16,9 @@ message Policy { required int64 published_at = 4; // Whether the policy is publicly readable or restricted to org members. - // Read at the edge (Fastly compute) to decide whether to enforce auth - // on the fetch. + // 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 = 5; // Categorical advisory rule. If set, deny any release whose maximum @@ -39,6 +40,7 @@ message Policy { } enum Visibility { + // PRIVATE is the safe default; unknown enum values must be treated as PRIVATE. VISIBILITY_PRIVATE = 0; - VISIBILITY_PUBLIC = 1; + VISIBILITY_PUBLIC = 1; } From bbda1761a8383785313d2e9a35f4e0eecb58b78f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eric=20Meadows-J=C3=B6nsson?= Date: Thu, 21 May 2026 01:19:17 +0200 Subject: [PATCH 3/7] Tighten Policy.name comment to match policy.md grammar --- registry/policy.proto | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/registry/policy.proto b/registry/policy.proto index c92bcf9..46c5a05 100644 --- a/registry/policy.proto +++ b/registry/policy.proto @@ -4,7 +4,8 @@ message Policy { // Name of repository required string repository = 1; - // Policy name within the repository (matches [a-z0-9_\-\.]+) + // 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) From c29594a46f7db76deba9de1de8536669f3690a4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eric=20Meadows-J=C3=B6nsson?= Date: Fri, 22 May 2026 17:03:23 +0200 Subject: [PATCH 4/7] Address Policy spec review feedback Lowercase MUST/SHOULD to match house prose style, note the deliberate int64-vs-Timestamp divergence between Policy.published_at and Release.published_at, drop overly-specific Content-Type detail from the doc, and signpost that cooldown composes by max-across where advisory/retirement compose by intersection. --- policy.md | 8 +++++--- registry/policy.proto | 3 +++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/policy.md b/policy.md index ad8e011..7a31873 100644 --- a/policy.md +++ b/policy.md @@ -10,7 +10,7 @@ A `Policy` is a signed resource published by an organization that the Hex client ## 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, served with `Content-Type: application/octet-stream` and `Content-Encoding: gzip`. +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 existing signing key — the same key already used to sign `/names`, `/versions`, and `/packages/NAME`. No new key infrastructure. @@ -23,7 +23,7 @@ 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. +If the payload cannot be decoded — signature mismatch, unknown enum value, missing required field — the edge must fail closed and require authentication. ## Rule semantics @@ -45,6 +45,8 @@ If `cooldown` is set and non-zero, the policy blocks any release whose `publishe 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: @@ -52,7 +54,7 @@ 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 already-locked versions. -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. +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 diff --git a/registry/policy.proto b/registry/policy.proto index 46c5a05..5ce06d1 100644 --- a/registry/policy.proto +++ b/registry/policy.proto @@ -14,6 +14,9 @@ message Policy { // Resource generation timestamp, seconds since unix epoch. // Clients use this to compute staleness when a fetch fails and the // last-known-good cached payload is loaded instead. + // (Deliberately int64 rather than the Timestamp submessage used by + // Release.published_at — resource-level generation time doesn't need + // sub-second resolution.) required int64 published_at = 4; // Whether the policy is publicly readable or restricted to org members. From 11bc1bd1b5b21252aadfe9d6e7c5eb8bd0b6d0c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eric=20Meadows-J=C3=B6nsson?= Date: Fri, 22 May 2026 17:21:34 +0200 Subject: [PATCH 5/7] =?UTF-8?q?Remove=20Policy.published=5Fat=20=E2=80=94?= =?UTF-8?q?=20unused=20by=20any=20client?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit No consumer reads the field: the Hex client switched its cache staleness check to file mtime, and the Fastly edge only reads visibility. Field 4 is reserved to prevent reuse. --- registry/policy.proto | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/registry/policy.proto b/registry/policy.proto index 5ce06d1..42206b8 100644 --- a/registry/policy.proto +++ b/registry/policy.proto @@ -11,13 +11,9 @@ message Policy { // Optional, free-form description (admin-set, surfaced in CLI/UI) optional string description = 3; - // Resource generation timestamp, seconds since unix epoch. - // Clients use this to compute staleness when a fetch fails and the - // last-known-good cached payload is loaded instead. - // (Deliberately int64 rather than the Timestamp submessage used by - // Release.published_at — resource-level generation time doesn't need - // sub-second resolution.) - required int64 published_at = 4; + // Field 4 was Policy.published_at — removed before release. + reserved 4; + reserved "published_at"; // Whether the policy is publicly readable or restricted to org members. // Read at the edge to decide whether to enforce auth on the fetch. From 1a566cf015421bc993298f111c71980c9b2aa965 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eric=20Meadows-J=C3=B6nsson?= Date: Fri, 22 May 2026 17:24:29 +0200 Subject: [PATCH 6/7] Renumber Policy fields contiguously after dropping published_at MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous commit reserved field 4 for back-compat. Nothing's been released, so the reservation is dead weight — renumber instead. --- registry/policy.proto | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/registry/policy.proto b/registry/policy.proto index 42206b8..387ba48 100644 --- a/registry/policy.proto +++ b/registry/policy.proto @@ -11,32 +11,28 @@ message Policy { // Optional, free-form description (admin-set, surfaced in CLI/UI) optional string description = 3; - // Field 4 was Policy.published_at — removed before release. - reserved 4; - reserved "published_at"; - // 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 = 5; + 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 = 6; + 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 = 7 [packed=true]; + 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 = 8; + optional string cooldown = 7; } enum Visibility { From d8df590a304fe2682cb90f24002ef2396b129fde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eric=20Meadows-J=C3=B6nsson?= Date: Fri, 22 May 2026 18:35:57 +0200 Subject: [PATCH 7/7] Remove temporal phrasing from Policy spec MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Docs read as if the system always existed — drop 'existing', 'already', and the 'no new key infrastructure' aside. --- policy.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/policy.md b/policy.md index 7a31873..84a40da 100644 --- a/policy.md +++ b/policy.md @@ -12,13 +12,13 @@ A `Policy` is a signed resource published by an organization that the Hex client 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 existing signing key — the same key already used to sign `/names`, `/versions`, and `/packages/NAME`. No new key infrastructure. +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 already access the repository. Same auth pipeline as `/packages/NAME` on a private repository. +* `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. @@ -53,7 +53,7 @@ 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 already-locked versions. +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