# Package Distribution — the self-hosted devpi Index

> **STATUS: FINALIZED — L1-binding.** The federation standard for `mgf-*` package
> distribution: **published to and resolved from the self-hosted devpi index**
> (`magogi/prod` on `195.15.203.250`), with `pypi.org` dropped as the `mgf-*`
> publish/distribution channel — third-party deps still resolve from public PyPI,
> served *through* devpi's `root/pypi` base (§1). Every endpoint, auth, and
> immutability fact below was **verified against the live index on 2026-07-01**:
> anonymous read serves `mgf-standard 0.1.60`, the index is a **non-volatile
> stage inheriting `root/pypi`**, and CI's publish/verify steps
> (`templates/woodpecker/base.yml`) round-trip against it.
>
> **Supersedes this doc's Codeberg-PyPI revision (2026-06-03).** Codeberg's
> package registry was the cutover's first target; Phase-2 B2 moved publish +
> resolve to the self-hosted devpi index (the same index CI resolves from). The
> running truth is [`MGF_INFRA.md`](../../MGF_INFRA.md) + the generated
> `templates/woodpecker/base.yml`. Where any lingering Codeberg-registry
> reference in the corpus disagrees, **devpi wins** — the §11 table tracks the
> sweep.

> **Conformance:** L1 (every Magogi-foundation project that publishes
> or consumes an `mgf-*` package).
> **Audience:** Human engineers AND AI coding agents acting as release
> maintainers or as supervisors applying a coupled change; consumers wiring an
> `mgf-*` dependency.
> **Propagation (DOC-18):** changing a rule here ripples —
> walk DOCUMENTATION.md §10.4 and update its dependents in the same commit
> (README *Index of MUSTs*, the `mgf-fed standards` / `agents`
> CLI, RELEASING.md / WORKFLOWS*.md, the release recipes, `.woodpecker.yml`, and
> any parallel copies / pins the matrix names).

> **Scope.** The federation's package **distribution channel**: where every
> `mgf-*` wheel/sdist is published, how it is authenticated, how a consumer
> resolves it, and how a release is verified. It **replaces `pypi.org` as the
> publish + distribution target for `mgf-*` packages** with the self-hosted
> **devpi** index (`magogi/prod`). It does **not** change
> what a package *is* (PEP 503 names, SemVer, the build) — those stay; only the
> *index* changes. See §1 for the one nuance that is not a full "drop": **third-
> party dependencies (pydantic, click, …) still resolve from public PyPI** — what
> we drop is publishing `mgf-*` to pypi.org and resolving `mgf-*` from it.

---

## 0. TL;DR — the one-screen version

**Index owner:** `magogi` (the devpi account that owns the `magogi/prod` index).
**Base:** `https://195.15.203.250.sslip.io/magogi/prod/` — a devpi **stage** index: non-volatile (published versions are immutable, §8) and inheriting `root/pypi` (so it serves `mgf-*` **and** proxies all of PyPI, §1).

| Action | Command |
|---|---|
| **Publish** (twine) | `TWINE_USERNAME=magogi TWINE_PASSWORD="$DEVPI_PASSWORD" twine upload --repository-url https://195.15.203.250.sslip.io/magogi/prod/ dist/*` |
| **Verify** a version is live | `curl -fsS https://195.15.203.250.sslip.io/magogi/prod/+simple/<name>/ \| grep -q <version>` (anonymous) |
| **Install** (single index) | `pip install --index-url https://195.15.203.250.sslip.io/magogi/prod/+simple/ <name>` (anonymous — resolves `mgf-*` + third-party) |
| **Inspect** a package | web: `https://195.15.203.250.sslip.io/magogi/prod/<name>` · JSON: `curl -H 'Accept: application/json' …/magogi/prod/<name>` |

- **Publish auth** = the `magogi` account + its upload password (CI: the tag-scoped Woodpecker org secret **`devpi_password`**). **Install is anonymous** — no token, no `.pypirc` credential.
- Because `magogi/prod` inherits `root/pypi`, a **single `--index-url`** resolves `mgf-*` (from the stage) **and** third-party deps (proxied from PyPI) — no `--extra-index-url`, no per-package index pinning needed for resolution. See §6 for the dependency-confusion nuance the single-index model introduces.

---

## 1. Why the self-hosted devpi index — and the one nuance

**Why devpi.** The federation runs its own infrastructure — devpi, Woodpecker CI,
the git forge — per [`MGF_INFRA.md`](../../MGF_INFRA.md) and rule **AG-20** (owned
infra self-recovers). A self-hosted index co-locates `mgf-*` distribution with the
CI that builds it, on hardware the federation controls: it drops a third-party
account + token lifecycle the federation doesn't own (the class of outage behind
the original pypi.org exit), keeps what are in practice **private federation
internals** off any public index, and — because the index is a devpi **stage
inheriting `root/pypi`** — gives the fleet a **single resolve endpoint** for both
`mgf-*` and their third-party dependencies (devpi caches the PyPI packages it
proxies, so a PyPI hiccup no longer blocks a fleet install).

**The one nuance — this is a channel switch, not "never touch PyPI".** `mgf-*`
packages depend on third-party libraries (`pydantic`, `pydantic-settings`,
`click`, …) that live on **public PyPI**. Those wheels still originate from PyPI —
but they now arrive **through** devpi's `root/pypi` base (proxied + cached), not
from a separate `--extra-index-url`. What we drop is precise:

- **DROP:** publishing any `mgf-*` package to pypi.org **or** the Codeberg
  registry (both are pre-devpi channels).
- **DROP:** resolving any `mgf-*` package *from* pypi.org or Codeberg (consumers
  pull `mgf-*` only from the devpi index).
- **DROP:** every pypi.org / Codeberg-registry URL, token, and Trusted-Publisher
  reference in the release tooling and standards as the `mgf-*` distribution
  mechanism.
- **KEEP:** public PyPI as the ultimate source for **third-party** dependencies —
  reached via devpi's `root/pypi` base, transparently.

Because the stage inherits `root/pypi`, a consumer points a **single
`--index-url`** at `magogi/prod/+simple/` and gets everything. The
dependency-confusion consideration this introduces — one index now serving both
`mgf-*` and public names — is addressed in §6 (PKG-06): the `mgf-*` prefix is a
private convention no public PyPI package uses, and a consumer MAY still pin
`mgf-*` to the stage explicitly for a hard guarantee.

---

## 2. Rules box (read this first)

| ID | Rule |
|---|---|
| <a id="PKG-01"></a>**PKG-01** | **Every `mgf-*` package MUST be published to, and resolved from, the self-hosted devpi index** (`magogi/prod` on `195.15.203.250`). Publishing an `mgf-*` package to `pypi.org` **or** the Codeberg package registry, or resolving an `mgf-*` dependency from either, is non-conformant. (Third-party deps still resolve from public PyPI — served *through* devpi's `root/pypi` base, §1.) |
| <a id="PKG-02"></a>**PKG-02** | **The canonical endpoints in §3 are the only ones used.** Upload → `https://195.15.203.250.sslip.io/magogi/prod/`; pip/uv index → `…/magogi/prod/+simple/`; verification → the per-package `…/magogi/prod/+simple/<name>/` listing (or the devpi JSON API `…/magogi/prod/<name>`). No hand-typed variants. |
| <a id="PKG-03"></a>**PKG-03** | **Publish auth is the `magogi` devpi account's upload password** (in CI, the tag-scoped Woodpecker org secret `devpi_password`); **read/install is anonymous** — the `+simple/` index needs no token. The publish password MUST follow REL-04 discipline — never pasted into chat/shell history/unmanaged env; local in a `chmod 600` file or `~/.pypirc`; in CI as a platform secret. There is **no OIDC / Trusted Publisher** — the token-in-CI-secret model is the standing answer, not a fallback. |
| <a id="PKG-04"></a>**PKG-04** | **The publish flow is strictly ordered** (§5): gates green → build → `twine check` → **upload to devpi** → **verify via the `+simple/` index (PKG-05)** → annotated tag `v<version>` → push tag. The wheel/sdist uploaded MUST be the exact artifact that was built and checked in this run (no rebuild between check and upload). |
| <a id="PKG-05"></a>**PKG-05** | **Post-publish verification MUST query the devpi index, never pypi.org or Codeberg.** A release is "live" only when it appears in `GET https://195.15.203.250.sslip.io/magogi/prod/+simple/<name>/` (the version string is listed), equivalently the devpi JSON API `…/magogi/prod/<name>`. Reading a local `dist/` wheel is never proof of publication (AG-09 still holds; the target is now devpi). |
| <a id="PKG-06"></a>**PKG-06** | **Consumers MUST resolve `mgf-*` from the devpi index via a single `--index-url`** (`…/magogi/prod/+simple/`), which serves `mgf-*` (from the stage) and third-party deps (proxied from `root/pypi`). Do **not** add `pypi.org` as a separate `--extra-index-url` — devpi already proxies it, and a second index reopens a dependency-confusion surface. Because the stage inherits `root/pypi`, the `mgf-*` prefix (a private convention no public PyPI package uses) is what keeps names unambiguous; a consumer MAY additionally pin `mgf-*` to the stage via `[tool.uv.sources]` for a hard guarantee (§6). |
| <a id="PKG-07"></a>**PKG-07** | **A published version is immutable — enforced, not just policy.** The `magogi/prod` stage is **non-volatile**: devpi rejects re-uploading an existing `name+version` (like pypi.org, unlike the old Codeberg registry). Botched-upload recovery is therefore an **admin action on the devpi host** (not a routine token op) — see §8 — and MUST be recorded (CHANGELOG note or release log). Never silently replace a version consumers may have resolved. |
| <a id="PKG-08"></a>**PKG-08** | **Package storage lives on the self-hosted devpi host — retention is host-ops hygiene.** There is no per-account cloud quota (unlike Codeberg); package data occupies the devpi host's disk (MGF_INFRA). Each package MUST retain at least its **current + previous minor**; older pre-release/superseded versions MAY be pruned by an admin (`devpi remove`, §8). Monitor the host's package volume as part of infra ops; grow the volume before it bites a release. |
| <a id="PKG-09"></a>**PKG-09** | **CI publishing MUST be tag-driven, build-in-CI, and secret-authenticated.** A `v*` tag push triggers: build → check → **upload-to-devpi** → **verify-devpi**. The publish password is a Woodpecker **org** secret (`devpi_password`) scoped to tag events; the upload user is `magogi`. No secret is ever echoed; no publish runs on non-tag events. The generated `templates/woodpecker/base.yml` publish + `verify-devpi` steps are the reference. |
| <a id="PKG-10"></a>**PKG-10** | **Naming defers to NM-01..NM-04** — hyphenated distribution names (`mgf-common`), dotted imports (`mgf.common`). devpi normalizes names per PEP 503 exactly as PyPI does. The canonical "where to find it" URL for an `mgf-*` package is its devpi page `https://195.15.203.250.sslip.io/magogi/prod/<name>`, not `pypi.org/project/<name>`. |
| <a id="PKG-11"></a>**PKG-11** | **Agent/federation version checks resolve against devpi (AG-09 amendment).** When a recommendation depends on a sibling's *published* version, the agent MUST verify against the devpi index (PKG-05: the `+simple/<name>/` listing), not pypi.org, not Codeberg, and not a local `dist/`. |
| <a id="PKG-12"></a>**PKG-12** | **No residual pypi.org (or Codeberg-registry) publish/distribution surface for `mgf-*`.** The only permitted `pypi.org` references in the corpus + tooling are (a) links to **third-party** packages and (b) clearly-historical notes; Codeberg remains the git forge but is **not** an `mgf-*` package registry. No `mgf-*` publish command, token, index, or verify step may target `pypi.org` or `codeberg.org/api/packages`. **Enforced** by the `no-pypi-surface` grep gate (§ Mechanical enforcement) — reference implementation: mgf-common `scripts/check_no_pypi_surface.py` + the `no-pypi-surface` CI step (the cornerstone template siblings copy). |

---

## 3. The index — canonical endpoints

Owner: **`magogi`** (the devpi account that owns the `magogi/prod` **stage**
index — non-volatile, inheriting `root/pypi`). **Verified live on 2026-07-01:**
anonymous read serves `mgf-standard 0.1.60`, and CI's publish + `verify-devpi`
steps round-trip against the endpoints below.

| Purpose | URL | Used by |
|---|---|---|
| **Upload (publish)** | `https://195.15.203.250.sslip.io/magogi/prod/` | `twine upload --repository-url …` (user `magogi` + `devpi_password`) |
| **Simple index (install)** | `https://195.15.203.250.sslip.io/magogi/prod/+simple/` | `pip --index-url …`, uv `[[tool.uv.index]] url = …` — **anonymous** |
| **Per-package index** | `https://195.15.203.250.sslip.io/magogi/prod/+simple/<name>/` | resolution + the lightweight "is it there?" check (PKG-05) |
| **JSON metadata** | `https://195.15.203.250.sslip.io/magogi/prod/<name>` (`Accept: application/json`) | version list / metadata |
| **Web page** | `https://195.15.203.250.sslip.io/magogi/prod/<name>` (browser) | human "where to find it" (PKG-10) |

**Read is anonymous** — the `+simple/`, JSON, and web endpoints need no
credential. **Upload** authenticates as the `magogi` account (HTTP basic: user
`magogi`, password `devpi_password`). Retention/removal (§8) is an admin
operation on the devpi host (`devpi remove`), not a REST DELETE.

---

## 4. Authentication

**Publish credential.** The `magogi` devpi account's **upload password**. devpi
authenticates uploads with an account username + password (HTTP basic); there is
no scoped-token model to mint — the single `magogi` account password *is* the
publish credential. **Read is anonymous** — installs need no credential at all.

**Storage (REL-04 discipline applies to the publish password):**

- **Local (manual publish):** a `~/.pypirc` (chmod 600), or a `chmod 600` file
  read into `TWINE_PASSWORD` at call time. Example `~/.pypirc`:
  ```ini
  [distutils]
  index-servers = devpi

  [devpi]
  repository = https://195.15.203.250.sslip.io/magogi/prod/
  username = magogi
  password = <the devpi upload password>
  ```
  then `twine upload --repository devpi dist/*`.
- **CI:** the Woodpecker **org** secret `devpi_password` (applies to every repo in
  org id 1), exposed only on tag events (§10). Never plaintext in `.woodpecker.yml`.
- **Consumers:** nothing — read is anonymous. No consumer token, no per-index
  credential to distribute or rotate.

**No Trusted Publisher / OIDC.** devpi has no PyPI-style Trusted Publisher; the
account-password-in-CI-secret model is the standing answer, not a fallback. Every
`pypa/gh-action-pypi-publish` / OIDC / `docs.pypi.org/trusted-publishers/`
reference is removed at cutover (§11).

> ⚠️ **Confirm (host-ops):** whether the `magogi` identity should be segmented —
> a dedicated CI upload account vs an admin account that can also `devpi remove`.
> Today one `magogi` account both uploads and administers `magogi/prod`;
> per-identity least-privilege is a possible hardening (it mirrors the old
> `write:package` / `read:package` split, now collapsed by anonymous read).

---

## 5. Publishing flow (the ordered ritual)

Same discipline, the devpi index. (CI does this automatically on a `v*` tag —
§10; this is the manual / reference form.)

1. **Gates green** — the project's full gate set (lint/format/type/imports/tests
   + coverage) passes.
2. **Bump** version in every coupled surface (pyproject + `__version__` + lock +
   CHANGELOG) — unchanged from today.
3. **Build** — `uv build` (or `python -m build`) → `dist/<name>-<ver>*`.
4. **`twine check dist/<name>-<ver>*`** — metadata still validates (the check is
   index-agnostic; keep it).
5. **Upload to devpi:**
   ```bash
   TWINE_USERNAME=magogi TWINE_PASSWORD="$DEVPI_PASSWORD" \
     twine upload --repository-url https://195.15.203.250.sslip.io/magogi/prod/ \
     dist/<name>-<ver>*
   ```
6. **Verify (PKG-05)** — poll the `+simple/` index until the version is listed
   (anonymous — no credential):
   ```bash
   until curl -fsS "https://195.15.203.250.sslip.io/magogi/prod/+simple/<name>/" \
     | grep -q "<name>-<ver>"; do sleep 5; done
   echo "<name> <ver> live on devpi."
   ```
7. **Tag + push** — annotated `v<version>` (REL-01), `git push origin v<version>`.
8. **Fresh-install smoke** — in a clean venv, install the *just-published* wheel
   from the devpi index (`--index-url …/magogi/prod/+simple/`, §6) and assert
   `__version__` + a content probe.

Ordering note: the `magogi/prod` stage is **non-volatile** — like pypi.org, a
`name+version` is one-shot. A botched step 5 is recovered only by an admin
`devpi remove` on the host (§8), so get the build right *before* uploading; treat
the version as immutable the moment step 6 confirms it.

---

## 6. Consuming / installing (single index, anonymous)

Both `mgf-*` and their third-party deps resolve from the **one** devpi index
(PKG-06) — no second index, no credential. The committed consumer config:

```toml
# pyproject.toml
[[tool.uv.index]]
url = "https://195.15.203.250.sslip.io/magogi/prod/+simple/"
default = true          # the single index for everything: mgf-* (stage) + third-party (via root/pypi)
```

Or at the command line (no committed config):

```bash
uv sync --index-url https://195.15.203.250.sslip.io/magogi/prod/+simple/
# or:  pip install --index-url https://195.15.203.250.sslip.io/magogi/prod/+simple/ <name>
```

**No credentials.** Read is anonymous — there is no `UV_INDEX_*` password, no
`read:package` token, nothing to inject in CI or bake into a lockfile. A CI
install simply points `--index-url` (or `UV_INDEX_URL` / `PIP_INDEX_URL`) at the
`+simple/` endpoint; the generated `templates/woodpecker/base.yml` is the reference.

**Do not add a second index.** Because `magogi/prod` inherits `root/pypi`, the one
endpoint already serves every third-party dependency (proxied + cached from PyPI).
Adding `pypi.org` as an `--extra-index-url` reopens the very dependency-confusion
surface a single trusted index closes.

> **Dependency-confusion under a single inheriting index.** devpi unions a name's
> releases from the stage and its `root/pypi` base. The `mgf-*` prefix is a
> private convention **no public PyPI package uses**, so an `mgf-*` name resolves
> unambiguously to our stage release. For a belt-and-braces hard guarantee, a
> consumer MAY additionally pin each `mgf-*` to the stage (name the index above,
> then `[tool.uv.sources] mgf-common = { index = "…" }`).
>
> ⚠️ **Confirm (policy):** whether PKG-06 should **mandate** explicit `mgf-*`
> stage-pinning, or whether the private-prefix convention + single-trusted-index
> is sufficient. The running fleet today uses the bare `--index-url` form
> (`templates/woodpecker/base.yml`), and the `chore/devpi-cutover` regen was
> tagged "confusion-safe" — reconcile the intended posture here.

**The old transitive-dependency trap is gone.** The Codeberg model needed
per-package `[tool.uv.sources]` mapping *and* direct-dependency re-declarations
because uv doesn't apply sources to *transitive* deps. The single-index devpi
model removes that trap entirely: every package — direct or transitive, `mgf-*` or
third-party — resolves from the one `--index-url`, so there is nothing to map and
nothing to re-declare. (One index-independent fact still holds: `mgf-common`'s
core has **no `mgf-*` runtime dependency** — the standards corpus ships as the
`mgf-common[standards]` extra — so most consumers pull no `mgf-*` transitively
anyway.)

---

## 7. Verify-after-publish (satisfies WF-08)

The only authoritative "is `<name> <version>` published?" check is the devpi
index, in increasing order of authority (all **anonymous**):

1. **Index presence:** `curl -fsS
   https://195.15.203.250.sslip.io/magogi/prod/+simple/<name>/` lists the files
   (fast, good for "did it land?"). This is the CI `verify-devpi` step's check.
2. **JSON metadata:** `curl -fsS -H 'Accept: application/json'
   https://195.15.203.250.sslip.io/magogi/prod/<name>` → the version appears in
   the release list (the canonical PKG-05 gate).
3. **Fresh-venv install:** install from the devpi index (§6) and import — the
   end-to-end proof a consumer can actually get it.

No step here may reference `pypi.org/pypi/<name>/json` or the Codeberg package API
for an `mgf-*` package.

---

## 8. Immutability, re-upload & retention (satisfies REL-05)

**The `magogi/prod` stage is non-volatile** — verified 2026-07-01
(`volatile: false`). Like pypi.org and **unlike** the old Codeberg registry, devpi
**rejects** uploading a `name+version` that already exists ("409 / version already
exists"). Two consequences:

- **Botched-upload recovery is an admin op, not a token op.** Recovering a bad
  upload (wrong metadata, missing file) requires **host access** to the devpi
  server: either temporarily set the index volatile, `devpi remove <name>==<ver>`,
  re-upload, and un-set volatile — or, on a strictly append-only prod, bump to the
  next patch version instead. This is the PKG-07 escape hatch — **recovery only,
  recorded (CHANGELOG / release log), never a silent content swap** of a version a
  consumer may hold.
  > ⚠️ **Confirm (host-ops):** the intended recovery procedure — is `magogi/prod`
  > strictly append-only (recover by version bump), or is a temporary
  > volatile-toggle + `devpi remove` sanctioned? Document the chosen one in
  > `MGF_INFRA.md`.

- **Retention is host-disk hygiene (no external quota).** Package storage lives on
  the self-hosted devpi host's disk (`MGF_INFRA.md`), not a per-account cloud
  quota. Define + run retention: keep at least **current + previous minor** of each
  `mgf-*`; prune older superseded/pre-release versions with an admin
  `devpi remove`. Track the host's package-volume usage as part of infra ops and
  grow the volume before it bites a release — there is no external allowance to
  request an increase against.

---

## 9. Naming (defers to NAMING.md NM-01..NM-04)

Hyphenated dist names, dotted imports — unchanged; devpi normalizes per PEP 503
just like PyPI. The only edit NAMING needs at cutover: the example "where it
lives" URL changes from `https://pypi.org/project/<name>/` to the devpi package
page `https://195.15.203.250.sslip.io/magogi/prod/<name>`, and NM-01's rationale
loses its pypi.org-display-drift framing (the hyphen convention still holds,
registry-agnostic).

---

## 10. CI (Woodpecker) — the publish/verify steps

The generated `templates/woodpecker/base.yml` is the canonical form (this is a
faithful sketch):

```yaml
  - name: publish               # tag-only
    image: python:3.12-slim
    when: [ { event: tag, ref: refs/tags/v* } ]
    environment:
      TWINE_USERNAME: magogi
      TWINE_PASSWORD: { from_secret: devpi_password }   # org secret, tag-scoped
    commands:
      - pip install --quiet --upgrade pip twine
      - twine upload --non-interactive
          --repository-url https://195.15.203.250.sslip.io/magogi/prod/ dist/*

  - name: verify-devpi          # tag-only — replaces verify-pypi / verify-codeberg
    image: python:3.12-slim
    when: [ { event: tag, ref: refs/tags/v* } ]
    depends_on: [publish]
    commands:
      - |
        python -c "
        import urllib.request, sys
        v = '${CI_COMMIT_TAG#v}'
        url = 'https://195.15.203.250.sslip.io/magogi/prod/+simple/<dist-name>/'
        html = urllib.request.urlopen(url, timeout=30).read().decode('utf-8','replace')
        sys.exit(0 if v in html else 1)
        "
```

Install is **anonymous** (`--index-url …/magogi/prod/+simple/`), so there is no CI
secret for the resolve side — only the `publish` step carries `devpi_password`.
`publish` depends on every gate (nothing red → no upload); `verify-devpi` depends
on `publish`. One-time setup: the `devpi_password` **org** secret (org id 1),
tag-event-scoped — it applies to every repo, so there is no per-repo secret wiring.

---

## 11. Mapping — what this supersedes (the wiring)

This devpi revision supersedes the **Codeberg-registry** model (this doc's
2026-06-03 finalization), which itself replaced pypi.org. The running system moved
first — `templates/woodpecker/base.yml` publishes to + resolves from devpi and
`templates/policy.toml` requires `verify-devpi` — so the corpus surfaces below are
the docs catching up, retargeted **Codeberg → devpi** in the same PAPER-112 sweep
as this doc: `WORKFLOWS.md` (WF-03/04/06/08), `RELEASING.md` (REL-04),
`WORKFLOWS_PATTERNS.md` §4, `AGENTS.md` (AG-09), `NAMING.md` (NM-01), `README.md`
(Index of MUSTs), the recipes (`cohort-release-wave`, `codeberg-pr`,
`standardized-git-config`, `run-ci`, `activate-ci`), and
`MGF_STANDARDS_CONFORMANCE.md` (the verdicts). `mgf-fed standards`/`agents` corpus
regen follows at the next release.

<details><summary>Historical record — the original pypi.org → Codeberg wiring audit (2026-06-03)</summary>

Applied as one DOC-18 change wave on finalization. Source: the repo-wide
pypi.org footprint audit.

| Surface | Today (pypi.org) | After (Codeberg) | Rule |
|---|---|---|---|
| `docs/standards/RELEASING.md` | "PyPI is the contract"; REL-04 `~/.pypirc`+Trusted-Publisher; REL-05 forever-immutable; REL-06 flow; `curl pypi.org/pypi/...` checks | Codeberg-is-the-contract; REL-04→PKG-03 token model; REL-05→PKG-07 (delete+re-push); REL-06→PKG-04 flow; REST verify | PKG-03/04/05/07 |
| `docs/standards/WORKFLOWS.md` | WF-03/04/06/08 PyPI publish + OIDC + poll pypi.org | retarget to Codeberg registry; drop OIDC | PKG-05/09 |
| `docs/standards/WORKFLOWS_PATTERNS.md` §4.2/4.3 | `twine upload` to PyPI; `from_secret: pypi_token`; "one-time pypi_token setup"; "Codeberg adds Trusted Publisher" line | Codeberg upload; `codeberg_package_token`; token-creation steps; drop the Trusted-Publisher aspiration | PKG-03/09 |
| `.woodpecker.yml` | publish (twine→PyPI, `pypi_token`) + verify-pypi comment | publish→Codeberg, `codeberg_package_token`; verify-codeberg step (§10) | PKG-09 |
| `docs/recipes/cohort-release-wave.md` | `uv publish` (PyPI token file); `curl pypi.org/.../json`; 429 first-publish cooldown | `uv publish --publish-url` Codeberg; REST verify; Codeberg quota/limits notes | PKG-04/05/08 |
| `docs/recipes/sibling_rename.md` | "publish under new name on PyPI"; "reserve old name on PyPI" | Codeberg publish; squatting risk differs on a private org (likely drop the reserve step) | PKG-01/10 |
| `docs/recipes/federation-sync-prompt.md` | "PyPI is the contract for latest released"; "curl PyPI" | Codeberg registry is the contract; REST/index check | PKG-05/11 |
| `docs/standards/AGENTS.md` AG-09 | verify against PyPI | verify against Codeberg registry | PKG-11 |
| `docs/standards/NAMING.md` NM-01 | `pypi.org/project/<name>` URL + display-drift framing | Codeberg package URL; registry-agnostic rationale | PKG-10 |
| `docs/standards/README.md` Index of MUSTs | WF-03/06/08, REL-01/03/04 rows naming PyPI | reworded for Codeberg + new PKG-* rows; `CHANGELOG.md` entry | DOC-18 |
| `scripts/gen_mgf_libraries.py` | comments "prefer a PyPI window"; "PyPI is the contract" | "Codeberg registry window/contract" (comments only; logic is registry-agnostic) | PKG-11 |
| `docs/standards/SECURITY_STANDARD.md` / `SECURITY.md` | "PyPI auto-yanks"; Trusted-Publisher | Codeberg delete/yank model; drop OIDC | PKG-07 |
| `docs/standards/DEPLOYMENT.md` DEP-03 | "no PyPI lookups at deploy" | "no registry lookups at deploy" (Codeberg too) | — |
| `docs/standards/COMMON_COMPONENTS.md`, `API_DESIGN.md`, `DESIGN_PRINCIPLES.md` | `pypi.org/project/...` links for `mgf-*` and 3rd-party | `mgf-*` → Codeberg; **3rd-party links stay** (apispec/qasync) | PKG-12 |
| `docs/standards/DOCUMENTATION.md` | "published to PyPI as a library" definitions | "published to the Codeberg registry" | PKG-01 |
| cookiecutter template `pyproject.toml` / docs / CI | (PyPI-ready, twine) | Codeberg index + uv sources scaffolding (§6) + Codeberg publish CI | PKG-06/09 |
| `.claude/settings.local.json` | `WebFetch(domain:pypi.org)` | add `codeberg.org`; keep pypi.org (3rd-party lookups) | — |
| conformance / adoption templates | release/publish sections | Codeberg publish + consume sections | PKG-* |

**New standards doc** `docs/standards/PACKAGE_DISTRIBUTION.md` (this file) +
README index rows for PKG-01..PKG-12 + a `CHANGELOG.md` entry +
`mgf-fed standards`/`agents` corpus regen at the next release.

</details>

---

## 12. Migration / cutover status (Phase-2 B2)

The devpi cutover **led with the running system**; the docs are the tail:

1. **CI resolve** — `templates/woodpecker/base.yml` installs via `--index-url …/magogi/prod/+simple/` (anonymous). ✅ (`chore/devpi-cutover`, PR #135.)
2. **CI publish** — the `publish` + `verify-devpi` tag steps target devpi with the `devpi_password` org secret. ✅
3. **Seed** — the fleet's current releases are on devpi (verified: `mgf-standard 0.1.60` served anonymously). ✅
4. **Docs** — this doc + the §11 corpus surfaces retargeted Codeberg→devpi. ◑ (this sweep — PAPER-112.)
5. **Decommission** — no `mgf-*` publish to pypi.org **or** the Codeberg registry; extend the `no-pypi-surface` grep gate to also reject `codeberg.org/api/packages` for `mgf-*` (enforcement above). ◑

---

## 13. Decisions (devpi model — verified 2026-07-01)

| Decision | Resolution |
|---|---|
| **Index** | The self-hosted **devpi** `magogi/prod` **stage** (non-volatile; inherits `root/pypi`) on `195.15.203.250`. Supersedes both pypi.org and the Codeberg package registry for `mgf-*`. |
| **Read auth** | **Anonymous** — no consumer token; a single `--index-url` serves `mgf-*` (stage) + third-party (proxied via the base). |
| **Publish auth** | The `magogi` account password; in CI the tag-scoped Woodpecker **org** secret `devpi_password`. No OIDC. |
| **Immutability** | Enforced — the stage is **non-volatile**; a `name+version` is one-shot. Recovery is a host-admin op (§8). |

**Open confirmations (flagged inline above):** the `magogi` identity split (§4), the botched-upload recovery procedure (§8), and whether PKG-06 should mandate explicit `mgf-*` stage-pinning (§6). These are the only non-mechanical decisions this retarget surfaces — everything else follows from the verified live model.

**Superseded — the 2026-06-03 Codeberg decisions:** `write:package` / `read:package` tokens (→ account password + anonymous read); REST delete-recovery (→ non-volatile immutability + host-admin `devpi remove`); the ~200 GiB account quota (→ host-disk retention); and the `UV_INDEX_CODEBERG_MGF_*` consumer credentials (→ no consumer credential at all).

---

## Mechanical enforcement

| Rule | Enforcement |
|---|---|
| PKG-01 / PKG-12 | **✅ Live** (mgf-common `scripts/check_no_pypi_surface.py` + the `no-pypi-surface` CI step, after `lint`; siblings copy the cornerstone). A grep gate: fails CI if `pypi.org` (or `codeberg.org/api/packages`) appears in a publish command, token ref, index URL, or verify step for an `mgf-*` package — allowlisting third-party references, comments, and historical notes. |
| PKG-02 / PKG-05 | The CI `verify-devpi` step (§10) — a release is not green until the version appears in the `+simple/` index. |
| PKG-03 / PKG-09 | The publish password is a tag-scoped Woodpecker **org** secret (`devpi_password`); a lint check that `.woodpecker.yml` has no plaintext secret and publish is `event: tag` only. |
| PKG-06 | A consumer-side check that committed config resolves from the devpi `--index-url` (single index; no bare `pypi.org` `--extra-index-url`). |
| PKG-08 | A host-ops job reporting devpi package-volume usage; retention prune (`devpi remove`). |

---

_Retargeted to the devpi index 2026-07-01 — L1-binding. Live-index facts verified end-to-end: anonymous read of `mgf-standard 0.1.60`; `magogi/prod` is a non-volatile stage inheriting `root/pypi`._
