# MGF_INFRA — the Magogi Foundation infrastructure & access registry

> **The authoritative inventory of what the Foundation *runs and owns*, and how to
> reach it.** Sibling of [`MGF_LIBRARIES.md`](MGF_LIBRARIES.md): that registry is
> "what we publish"; this is "what we operate." Maintained by the mgf-common
> supervisor; keep it current as infra changes.
>
> **Why this exists (read before declaring anything "blocked").** Agents kept
> treating a transient failure against *our own* infrastructure — an expired
> Woodpecker token, a 401, a stopped container — as a terminal, human-gated wall.
> It never is. **If a dependency is in this registry, it is Magogi-owned: you have
> an access path and a recovery path. Reason from capabilities (what we control +
> how to reach it), not from a recorded "blocked" conclusion.** This is binding as
> **AG-20** (see [docs/standards/AGENTS.md](docs/standards/AGENTS.md)).
>
> **No secrets here.** This file is committed. It records *where* credentials live
> (always under a repo's git-ignored `my_stuff/`, AG-18) and *how* to obtain a
> token — never the values themselves.

## §1 What we own (inventory)

All VMs run on **Infomaniak Public Cloud** (OpenStack, project `PCP-HOESFJV`,
region `dc3-a`), reachable over `ext-net1` direct-public, fronted by Caddy + Let's
Encrypt. **One SSH key reaches every host:**
`mgf-standard/my_stuff/infomaniak/woodpecker_id_ed25519` (OpenStack keypair
`woodpecker-key`), user `ubuntu`.

| Service | Role | URL | Host (SSH `ubuntu@…`) | Self-hosted? |
|---|---|---|---|---|
| **Woodpecker CI** | the fleet's CI/CD (`woodpecker-prod`, v3.8.0; server+agent+postgres+caddy) | https://195.15.241.48.sslip.io | `195.15.241.48` | ✅ ours |
| **devpi index** | the private PyPI the fleet publishes to + resolves from (`magogi/prod`) | https://195.15.203.250.sslip.io/magogi/prod/+simple/ | `195.15.203.250` | ✅ ours |
| **OpenObserve** | central logs (app→logship→collector→OO) | https://195.15.203.114.sslip.io | `195.15.203.114` | ✅ ours |
| **Codeberg** | the forge (org `magogi-admin`; repos, PRs, package webhooks) | https://codeberg.org/magogi-admin | — (SaaS, but **our org**) | external, owned-account |
| **Infomaniak** | the cloud provider (VMs, storage, networking) | https://manager.infomaniak.com · https://dashboard.pub1.infomaniak.cloud | — | external, owned-account |

Cost: the VMs run 24/7 (~€38/mo); tracked by `mgf-cost-watch` (Woodpecker cron,
repo id 25). Tear-down/resize via `mgf-cloud vm …`.

## §2 Access & recovery paths (the "you are not blocked" details)

### Woodpecker CI (the one that bit us)
- **Codeberg commit-status contexts:** `ci/woodpecker/{pr,push,tag,manual}/woodpecker`. Branch protection requires `ci/woodpecker/pr/woodpecker`.
- **API token (the recurring gate):** the stored token (`my_stuff/woodpecker_token.txt`) is a JWT that **expires** (→ 401). **Do not treat that as blocked, and do not need a browser/OAuth.** Mint a fresh admin token from the host (it's ours):
  1. `ssh -i my_stuff/infomaniak/woodpecker_id_ed25519 ubuntu@195.15.241.48`
  2. A v3 user/CLI token is an **HS256 JWT, claims `{"type":"user","user-id":"<id>"}`, signed with the user's `hash`** (NOT the global `jwt-secret`). Get id+hash: `sudo docker exec woodpecker-woodpecker-postgres-1 psql -U woodpecker -d woodpecker -tAc "select id, hash from users where login='magogi-admin'"` (magogi-admin = id 1, admin). Hand-roll the JWT (hmac-sha256, base64url no padding). `GET /api/user` → 200. *(Verified against v3.8.0 `shared/token/token.go` + `server/api/user.go`.)*
- **Activate a new repo's CI:** `POST /api/repos?forge_remote_id=<codeberg-repo-id>` (get the id from `GET codeberg.org/api/v1/repos/<owner>/<repo>`).
- **Publish a release:** push a `v*` tag → the repo's pipeline runs the publish step using the **org-level** `devpi_password` secret (`events:["tag"]`, applies to all repos in org id 1). No manual twine needed.
- **Agent wedge (worker_count=0):** `cd /opt/woodpecker && sudo docker compose restart woodpecker-agent` (a `*/5` self-heal cron also exists). Server config: `/opt/woodpecker/.env`.
  - ⚠️ **Known gap (FEEDBACK.md PAPER-111):** the `*/5` self-heal did **not** clear an observed 15-min wedge (queue `worker_count:0, pending:13, running:4`) — it cleared only on the **manual** restart above. Likely the cron detects *container-down*, not the *`workers==0`-with-a-backlog* wedge shape (or it restarts-but-re-wedges). **Host-side fix to apply:** (1) verify the cron actually runs (`crontab -l`; inspect its log); (2) make its trigger the **wedge condition** — restart when `worker_count==0 && pending>0` (poll `GET /api/queue/info` with an admin token), not just container-down; (3) **emit a signal** (an OpenObserve record / push) whenever it fires *or* a wedge persists > N min, so a silent-failing self-heal is itself observable; (4) optionally a compose `healthcheck` that restarts the agent on a failed heartbeat. Until deployed, a wedge = the manual restart above.

### devpi (publish / inspect)
- Releases land here via the Woodpecker tag pipeline (above). The upload account is `magogi`; the password is the Woodpecker org secret `devpi_password` (not stored in any repo). Anonymous read at the `+simple/` index. Host SSH for server-side ops.
- **`$DEVPI_URL` (CI indirection, PAPER-113):** generated pipelines never hardcode the index endpoint — they reference the Woodpecker **server-level** variable `DEVPI_URL` (injected into every step via `WOODPECKER_ENVIRONMENT` in `/opt/woodpecker/.env` + compose; set 2026-07-04; value = the §1 endpoint `https://195.15.203.250.sslip.io/magogi/prod/+simple/`), with a fail-fast guard line in each consuming step. Variable name is coordinated with `sooth-standard` (their conversion shipped first, live-green). A devpi host/IP move is therefore: update the server variable + the §1 row here + `mgf-fed`'s `_DEVPI_INDEX` constant (`mgf.fed._woodpecker` — the generator-side installs still carry the literal until that companion lands), then run the WF-13 re-render wave — never a hand-search across committed pipelines.

### Codeberg (forge)
- API token: `~/.config/tea/config.yml` (used by `tea` + the REST API). Org repos are **private**; branch-protect via `POST /repos/<owner>/<repo>/branch_protections`.

## §3 Discipline (AG-20)

Before reporting an action as **blocked / human-gated / impossible**:
1. **Check this registry** — is the dependency Magogi-owned?
2. **If owned, attempt the self-owned recovery path** above (SSH, mint/rotate a credential, restart a service). An auth/access failure on owned infra is a *solvable sub-problem*, not a terminal wall.
3. **If it genuinely can't proceed, state the blocking assumption explicitly** ("treating X as blocked because Y") so a human can catch a wrong assumption cheaply.
4. **Recorded "blocked"/"gated" notes** (memory, trackers) MUST carry their unblock-path + as-of date, and be **re-derived against current capabilities** — never inherited as settled fact.

## §4 Maintenance

Update when infra is added/removed/moved, a host IP changes, or an access/recovery
path changes. Per-host live details live in each consumer's git-ignored
`my_stuff/` (e.g. `mgf-standard/my_stuff/infomaniak/FACTS.md`); this file is the
fleet-level index + the access/recovery playbook. Related standards: **AG-20**
(this discipline), **AG-18** (`my_stuff/` git-ignored), **PKG-06** (devpi index),
**WF-13** (generated CI).
