CANP-WP-0002 T02: registry-scoped identity and namespace ownership

Section 17 already defined what a registry does when the same id@version is
republished; nothing defined what a *consumer* does. That is where namespace
conflict actually bites — in the catalog, after installing from two
registries.

Identity is now registry-scoped: an id names a package within a registry, the
way a path names a file within a repository. A local-first format with no
signing, no federation and no central authority cannot enforce global
uniqueness, and an unenforceable guarantee is worse than none — it invites
consumers to conflate two packages that merely share a name. A qualified
`<registry>:<id>` reference distinguishes them, and `:` is now barred from ids
so the separator stays available.

Installing the same id from two registries is therefore not a conflict. The
catalog is namespaced by registry and keeps both.

Ownership is registry policy, not package data. An optional `registry.yaml`
names a registry and records namespace claims. Those claims are explicitly
descriptive — a filesystem registry cannot authenticate a publisher, and
`publish` says so rather than implying it checked. Keeping the claim out of
packages leaves artifacts free of unverifiable assertions of authority, and
means package semantics do not change when a hosted registry appears later.

Spec: 3.2 (registry-scoped identity, qualified references), 17 (immutability
scoped to a registry), 20.1 and 20.2 (new), 18 (registry-manifest
validation), 21 (qualified references, reserved `local` name).

Reference CLI: parse_reference, check_registry_name, read_registry_manifest,
registry_name, namespace_policy; registry_package_path and
catalog_package_path split; resolve_installed reports ambiguity and returns
the source registry; iter_catalog; `add --as`; closed-namespace warning on
publish. Tests 11 -> 21.

The catalog layout changed. An existing catalog is detected and reported with
instructions rather than failing as "package not found". Signing, trust
scoring and federation remain non-goals and were not touched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bjefh8NUiEiahN4JLwoSKM

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 388925@bnt-lap001
Assistant-Session: 3507023f-e0fd-4a1e-9d90-a0d4217d1502
This commit is contained in:
tegwick 2026-09-06 01:14:54 +02:00
parent 4a8422e1aa
commit ea70a59610
6 changed files with 533 additions and 45 deletions

View file

@ -119,8 +119,42 @@ Rules for v0.1:
- `/`, `-`, `_`, and `.` MAY be used;
- whitespace MUST NOT be used;
- the ID MUST NOT contain `..` path traversal segments;
- `:` MUST NOT be used, so that it remains available as the registry separator
in a qualified reference (below);
- registry implementations MUST treat the ID as logical metadata rather than an unchecked filesystem path.
##### Identity is registry-scoped
An `id` names a package **within a registry**, the way a path names a file
within a repository. CPF v0.1 does **not** claim that an id is globally unique:
`practice/pqrst-estimate` obtained from two different registries may be two
different packages, and a consumer that draws on more than one registry MUST
keep track of which registry each package came from.
This follows from what the format actually guarantees. A local-first format
with no signing, no federation and no central authority (all explicit
non-goals) cannot enforce global uniqueness, and a guarantee that cannot be
enforced is worse than none — it invites consumers to conflate two packages
that merely share a name.
Where a consumer must distinguish them, a **qualified reference** names the
registry:
```text
<registry>:<id>
```
For example:
```text
house:practice/pqrst-estimate
upstream:practice/pqrst-estimate
```
An unqualified id is acceptable wherever it is unambiguous for that consumer.
A tool that finds the same id in more than one registry MUST report the
ambiguity rather than choosing for the caller.
#### `name`
Human-readable display name.
@ -602,6 +636,12 @@ Published `<id>@<version>` pairs SHOULD be immutable.
A registry SHOULD reject publication of a package when the same ID/version already exists with different contents unless an explicit administrative override mechanism exists.
Immutability is scoped to a registry, because identity is (§ 3.2). The same
`<id>@<version>` held by two registries is two packages, and a consumer holding
both is not in a conflict — it is holding two things whose names happen to
coincide. Only a repeat publication *within one registry* violates
immutability.
Suggested versioning guidance:
- PATCH: wording/metadata correction with intended behavior unchanged;
@ -632,6 +672,12 @@ A v0.1 validator SHOULD verify at least:
A validator SHOULD additionally warn when a package intended for publication
declares an inline derivation prompt (§ 6.1).
A validator that is given a **registry** rather than a package SHOULD verify,
when `registry.yaml` is present, that `format` is
`canned-prompt-registry/v0.1`, that `name` is a non-empty string usable in a
qualified reference, and that each `namespaces` entry declares a `policy` of
`open` or `closed`.
## 19. Security requirements
Prompt packages are content, not trusted code.
@ -675,6 +721,70 @@ Conceptually, a registry stores immutable package versions keyed by:
<id>@<version>
```
### 20.1 Registry identity and namespace ownership
A registry MAY declare itself with a `registry.yaml` at its root:
```yaml
format: canned-prompt-registry/v0.1
name: house
description: Internal prompt registry.
namespaces:
practice:
owner: Ada Example
policy: closed
scratch:
policy: open
```
Fields:
| Field | Required | Meaning |
|---|---:|---|
| `format` | yes | MUST be `canned-prompt-registry/v0.1` |
| `name` | yes | Short registry name, used in qualified references (§ 3.2) |
| `description` | no | Human-readable explanation |
| `namespaces` | no | Mapping of namespace to its claim |
Within `namespaces`, `owner` is a human-readable claim and `policy` is
`closed` (only the owner publishes) or `open` (anyone may). Both are
descriptive: a filesystem registry has no way to authenticate a publisher, and
authentication, signing and trust scoring are all explicit non-goals.
The file is **optional**. A bare directory remains a valid registry; a consumer
that finds no manifest SHOULD take the registry's name from how it was
addressed — for the reference implementation, the registry directory's
basename.
**Ownership is a property of the registry, not of the package.** A package
never declares who owns its namespace. This keeps the claim where it can
actually be acted on — the registry decides what it admits — and keeps
packages free of unverifiable assertions of authority, consistent with § 19's
position that a package is content rather than a trusted actor. It also means
package semantics do not change when a filesystem registry is later replaced
by a hosted one.
A registry SHOULD refuse to publish into a `closed` namespace it does not
consider the publisher to own. How it decides is registry policy and is
outside this specification.
### 20.2 Consumer-side layout
A consumer drawing on more than one registry MUST keep packages from different
registries distinct, because identity is registry-scoped (§ 3.2). Installing
the same `<id>@<version>` from two registries is not an error and MUST NOT
overwrite: both are retained and addressed by qualified reference.
The reference implementation stores its catalog as:
```text
catalog/
└── <registry name>/
└── <id path>/
└── <version>/
```
The reference implementation uses the filesystem layout:
```text
@ -689,6 +799,7 @@ For example:
```text
registry/
├── registry.yaml
└── practice/
└── pqrst-estimate/
└── 0.1.0/
@ -696,6 +807,9 @@ registry/
└── prompt.md
```
A registry's own layout is not namespaced by registry name: within one
registry, an id is unambiguous by definition.
## 21. Reference CLI semantics
The v0.1 reference tool uses two stores:
@ -715,6 +829,14 @@ publish PATH validate and copy a package into a filesystem registry
install ID copy a package version from the registry into the catalog
```
Every command that takes an `ID` accepts either a bare id or a qualified
`<registry>:<id>` reference (§ 3.2). A bare id that matches packages installed
from more than one registry is reported as ambiguous, listing the candidates,
rather than resolved by guessing.
`add` takes a package from a path rather than from a registry, so it files the
package under the reserved registry name `local`.
The reference tool never calls a model, so its `resolve` handles supplied
values and static defaults only and reports any input whose derived default it
cannot satisfy. `render` performs the same resolution and then substitutes;