# muninn-gate

WG-internal HTTP front door for the Muninn ingestion queue (Worldtree #377).
Path-addressed submit, list/status, cancel/retry, watcher-liveness health.
Upstream: `vh/muninn-gate` (no Dockerfile there — this stack owns containerization).

## Where it runs

**corviduo-dev (10.250.50.152)**, co-located with the worldtree-personal muninn
watcher (`worldtree-personal-worldtree-muninn-1`). infra-ops-managed stack,
separate from the worldtree CI/CD compose. Bind: `10.250.50.152:8090`, `network_mode: host`.

## Key wiring (why it's shaped this way)

- **`ingestion_root: /data/state/ingestion`** — the `worldtree-personal_worldtree-state`
  volume, mounted at `/data/state`. Byte-identical path to the watcher's view; the
  dispatch records absolute paths and both processes read each other's.
  Acceptance: `/health` → `watcher.running: true` **proves** this byte-identity
  (gate is reading the heartbeat the watcher writes). `no_heartbeat` with the
  watcher up = root mismatch.
- **`user: "1000:1000"`** — the ingestion dir is `vh:vh 0755`; the gate must run as
  uid 1000 to write the queue (non-root, least-privilege). Overrides the image user.
- **staging** — `/mnt/muninn-staging/mimir-inbox`, same absolute path bound `:ro` in
  BOTH the gate and the watcher (worldtree-dev's b162). Currently a LOCAL placeholder
  dir; becomes the shared mount when mimir-inbox (the writer) lands. **No gate-side
  check can tell a real share from an empty dir** (docker fabricates a missing bind
  source) — the guard is operational: confirm the host mount exists before wiring/
  repointing a bind. Repoint needs a restart (config is read once at boot).
- **health probe uses `/ping`, not `/health`** — `/health` is always-200 by design
  (watcher-down is report *content*, not a status code), so it must never be the
  liveness probe or it would restart the wrong container.

## Config (single-writer, infra-ops)

Real config with bearer-key secrets lives on the server at
`/opt/docker/conf/muninn-gate/muninn-gate.yaml` (`1000:1000`, `0600`, gitignored).
Redacted schema: `conf/muninn-gate.example.yaml`. Schema is CLOSED — any unknown
field is a boot failure. Keys are flat-scope (`read | submit | control`, no
inheritance); `name` is the non-secret `submitted_by` identity.

## Build + deploy

Image builds out-of-band — the Gitea read token (for `muninn-dispatch==0.1.4` from
the internal index) rides as a BuildKit secret, never a layer:

```bash
DOCKER_BUILDKIT=1 docker build --secret id=gitea_pw,src=<gitea-read-token> -t muninn-gate:0.0.14 .
docker compose up -d          # after installing the config to /opt/docker/conf/muninn-gate/
```

## Status

Booted + healthy 2026-07-31; `/ping` + `/health` (watcher:running:true,
ingestion_root_writable:true) verified. **SUBMIT deferred** — returns `not_found`
against the placeholder staging until the real shared mount + mimir-inbox writer
exist; then repoint + the one-file path-agreement probe + the real acceptance run.
