Retention plans
A retention plan evaluates one metadata snapshot and returns an ordered decision for each artifact. Evaluation changes no metadata and deletes no content. Administrators can inspect the result through the CLI or management API before a separate apply step uses it.
The planner reads hosted records. Cache eviction manages upstream cache state, and blob reclamation removes unreferenced bytes. Neither operation changes retention decisions.
Rules
A policy has ordered keep and expire groups. A matching keep rule protects an artifact. If none matches, the first
matching expire rule makes it eligible for removal.
Available selectors include:
age: recorded time is at leastolder_than_secondsbefore the evaluation clocksource: content came from the named sourceresource-prefix: the resource starts withprefixkeep-latest-groups: content belongs to one of the newestcountgroups for its resourcecached: content came from an upstream cachetrash: content has a restorable deletion recordorphan: no live metadata reference reaches the contentvisibility: content has the named owner-defined visibility state
An age rule does not match an artifact without a recorded time or an evaluation without a clock.
PyPI support
Hosted PyPI repositories support age, resource-prefix, keep-latest-groups, trash, and visibility. They reject
source, cached, and orphan because hosted upload records do not carry those facts. Peryx handles cached content
through cache eviction and unreferenced content through blob reclamation.
keep = [
{ selector = "keep-latest-groups", count = 10 },
{ selector = "age", older_than_seconds = 2592000 },
]
expire = [
{ selector = "trash" },
{ selector = "resource-prefix", prefix = "scratch-" },
{ selector = "visibility", state = "hidden" },
]Precedence
The first matching keep rule wins over all expire rules. Without a keep match, the first expire match wins. If no
rule matches, the planner retains the artifact and reports no deciding rule.
This precedence follows Google Artifact Registry cleanup policies.
Ecosystem ordering
The ecosystem retention capability ranks groups for keep-latest-groups and supplies a stable identity for equivalent
groups. The planner orders groups by that rank, then uses artifact and digest as tie-breaks.
Decisions
Each decision contains resource, group, artifact, digest, storage class, and logical visibility. These field
names form the shared planner schema; ecosystem owners map their subjects and content types onto them.
A removal decision also contains:
outcome:remove, compared withretainrule: selector that produced the decisionbytes: estimated physical sizeretained_groups: groups left after the planned removal
The output order is total. Evaluating the same snapshot and policy produces the same bytes.
Plan identity
A plan identifies both inputs:
policy_version: stable hash of the compiled, typed rule valuesfrontier: repository revision, catalog generation, and policy generation read by the scan, all scoped to this repository
An apply step can reject a plan after either input changes.
Read-only execution
Evaluation opens read transactions and does not enumerate backend blobs. It groups and emits one subject at a time, and
within a subject it expands one decision at a time: retained_groups repeats every surviving group in every removal, so
holding the whole subject's decisions at once would cost removals times survivors. A cancelled request or process exit
leaves repository state unchanged.
An ecosystem's per-subject memory budget bounds that live set — the candidates, the classified state, the surviving-group index, and the one decision being expanded — not what the plan serializes to. A subject whose response is far larger than the budget still streams.
HTTP preview
POST /+retention/plan returns one page. POST /+retention/export streams the complete plan as
JSON Lines with the summary first. Both require a local administrator. An unauthorized caller
receives 404 and cannot use errors to infer repository contents.
Example request:
{
"repository": "team-hosted",
"keep": [
{
"selector": "keep-latest-groups",
"count": 3
}
],
"expire": [
{
"selector": "age",
"older_than_seconds": 7776000
}
],
"limit": 100
}
Example page:
{
"summary": {
"policy_version": 42,
"frontier": {
"repository": 7,
"catalog": 3,
"policy": 2
}
},
"candidates": [
{
"resource": "example",
"group": "1.0",
"artifact": "example-1.0.bin",
"digest": "<digest>",
"class": "hosted",
"visibility": "active",
"bytes": 20480,
"outcome": "remove",
"rule": "age",
"retained_groups": [
"2.0"
]
}
],
"next_cursor": null
}
The export response uses the plan identity as its ETag. A later apply can send that value with If-Match.
CLI preview
peryx retention dry-run prints one page of tab-separated candidates, followed by a summary and optional cursor.
peryx retention export writes the JSON Lines form. Both read the local store and load rules from a TOML file. Without
a rules file, the plan retains all content.
$ peryx retention dry-run --index team-hosted --rules retention.toml --limit 100
$ peryx retention export --index team-hosted --rules retention.toml > plan.jsonlPagination and limits
A cursor binds its offset to the plan identity. A changed snapshot causes 409 Conflict over HTTP or a stale-cursor
error in the CLI. Export resumes from a page boundary by using that page cursor. The response advertises
Accept-Ranges: none because byte ranges cannot identify plan boundaries.
A page holds at most its requested limit. Export buffers one candidate and applies backpressure when the reader stalls.
Each repository permits a fixed number of concurrent plans; excess requests receive 429 Too Many Requests.