# 2026-02-28 – Modrinth Install + Full Mod Lifecycle

**Type:** Foundational Architecture + Implementation
**Status:** Full mod lifecycle complete, file browser next

---

## Completed

### API

Implemented full mod lifecycle routes:

```
GET    /api/game/mods/search?q=&vmid=
GET    /api/game/servers/:id/mods
POST   /api/game/servers/:id/mods/install
PATCH  /api/game/servers/:id/mods/:modId
DELETE /api/game/servers/:id/mods/:modId
```

All routes:
- `requireAuth`
- Enforce container ownership
- Enforce `ctype === "game"`
- Forward to agent with timeout + `502`/`504` mapping

### Resolver

- Loader normalization fixed (`neoforge` / `fabric` / `forge` / `quilt`)
- `.jar`-only enforcement
- Host allowlist: `cdn.modrinth.com`
- Publish-date safe sorting
- SHA512 preferred (fallback to SHA1)

### Agent

Mod install flow:

- Accepts direct Modrinth artifact URL
- Validates:
  - HTTPS only
  - Allowed host (`cdn.modrinth.com`, `artifacts.zerolaghub.com`)
  - Max size 200MB
  - SHA256 hash
- Writes to `<serverRoot>/mods`
- Enables via filename convention
- Soft delete moves to `<serverRoot>/mods-removed`
- Enable/disable via rename: `.jar` ↔ `.jar.disabled`

### Frontend

- Mod search drawer implemented
- Installed mods panel implemented
- Installed flag merged into search results (heuristic matching)
- Install button functional
- Enable / disable functional
- Delete functional
- Toast notifications added

---

## Current System Behavior

Filesystem is source of truth.

No database persistence for:
- Mod install history
- Modrinth project ID mapping

Installed matching is heuristic based on:
- Slug
- Filename
- Name

Soft delete retains file permanently in `/mods-removed`. No retention policy implemented (intentional).

---

## Known Architectural Tradeoffs

- No deterministic Modrinth project ID persistence yet
- Installed detection is best-effort heuristic
- No install queue
- No auto-update logic

---

## Security Controls In Place

**Agent level:**
- HTTPS-only downloads
- Host allowlist enforcement
- Redirect limit (max 3)
- 200MB hard cap
- SHA256 verification
- Filename sanitization (`[a-zA-Z0-9.+\-]` — `+` added for Modrinth filenames)
- Duplicate prevention
- Ownership enforcement (`minecraft:minecraft`)
- Temp file cleanup on failure
- Controlled mod directory write path
- Cache invalidation after every mutation

**API level:**
- Auth + ownership enforcement
- VMID validation
- Timeout protection with `AbortController`
- Resolver filtering (loader + version + stability)
- Correct payload contract to agent

---

## Payload Contract (API → Agent)

```json
{
  "source": "modrinth",
  "download_url": "...",
  "filename": "...",
  "sha512": "..."
}
```

Engine metadata format (corrected this session):

```
engineType    = "neoforge"   (not "minecraft")
engineVersion = "1.21.4"     (not "neoforge-1.21.4")
```

---

## Remaining Issues

### Response Corruption (Needs Verification)

One early `curl` output appeared corrupted. Reproduce before wiring portal:

```bash
curl -sSl -D headers.txt -o body.txt ...
```

### API Error Mapping Refinement

- `"mod already exists"` should return `409` (currently `502`)

---

## Next Steps

1. Reproduce and resolve response corruption issue
2. Tighten API error mapping (`409` for duplicate)
3. Wire install endpoint to portal UI
4. Begin file browser (see `OPEN_THREADS.md`)
