{
  "openapi": "3.1.0",
  "info": {
    "title": "punktfunk management API",
    "description": "Control-plane API for managing a punktfunk streaming host: host capabilities, runtime status, paired clients, the pairing PIN flow, and session control. Authentication: HTTP bearer token, enforced on every route except `/api/v1/health` when the host is started with a management token (mandatory for non-loopback binds).",
    "contact": {
      "name": "unom"
    },
    "license": {
      "name": "MIT OR Apache-2.0",
      "identifier": "MIT OR Apache-2.0"
    },
    "version": "0.20.0"
  },
  "paths": {
    "/api/v1/clients": {
      "get": {
        "tags": [
          "clients"
        ],
        "summary": "List paired clients",
        "operationId": "listPairedClients",
        "responses": {
          "200": {
            "description": "All certificate-pinned clients",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/PairedClient"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/clients/{fingerprint}": {
      "delete": {
        "tags": [
          "clients"
        ],
        "summary": "Unpair a client",
        "description": "Removes the client's certificate from the pairing store. Caveat: the nvhttp TLS layer\ndoes not yet reject unlisted certificates (`gamestream/tls.rs` accepts any well-formed\nclient cert — a planned hardening step), so until that lands this removes the client\nfrom the listing without severing its ability to reconnect.",
        "operationId": "unpairClient",
        "parameters": [
          {
            "name": "fingerprint",
            "in": "path",
            "description": "Hex SHA-256 fingerprint of the client certificate DER (64 chars, case-insensitive)",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Client unpaired"
          },
          "400": {
            "description": "Malformed fingerprint",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No paired client with that fingerprint",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/compositors": {
      "get": {
        "tags": [
          "host"
        ],
        "summary": "Available compositor backends",
        "description": "Lists every backend the host knows how to drive, flags which are usable right now, and marks\nthe one an unspecified (`Auto`) client request resolves to. Clients pass an `id` to their\n`--compositor` flag (or `PUNKTFUNK_COMPOSITOR_*` over the C ABI) to request it.",
        "operationId": "listCompositors",
        "responses": {
          "200": {
            "description": "Compositor backends with availability + the auto-detected default",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/AvailableCompositor"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/display/layout": {
      "put": {
        "tags": [
          "display"
        ],
        "summary": "Arrange virtual displays",
        "description": "Set the **manual** desktop arrangement — per-identity-slot `(x, y)` offsets so a multi-monitor\ngroup (§6A/§6B) comes back where the operator placed it. Persisted into the policy's layout block\nand switched to manual mode; applied from the next connect (a live group re-applies on its next\nacquire). Locks in the current effective behavior as explicit fields, so arranging displays never\nsilently changes keep-alive/topology/conflict/identity. See `design/display-management.md` §6.2.",
        "operationId": "setDisplayLayout",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DisplayLayoutRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "Layout stored; the new settings state",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DisplaySettingsState"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "500": {
            "description": "Layout could not be persisted",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/display/presets": {
      "get": {
        "tags": [
          "display"
        ],
        "summary": "List the saved custom presets",
        "description": "The operator's named field-bundles (`display-presets.json`). These also ride the\n`GET /display/settings` response (`custom_presets`), so the console rarely needs this directly.",
        "operationId": "listCustomPresets",
        "responses": {
          "200": {
            "description": "The saved custom presets",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/CustomPreset"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "display"
        ],
        "summary": "Save a custom preset",
        "description": "Stores a named bundle of the display-behavior axes (+ the game-session axis) the operator can\napply later. The host assigns a stable id, returned in the body. Applying a preset is a\n`PUT /display/settings` with a `Custom` policy carrying its `fields` — no separate apply route.",
        "operationId": "createCustomPreset",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CustomPresetInput"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "description": "Preset created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CustomPreset"
                }
              }
            }
          },
          "400": {
            "description": "Empty name",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "500": {
            "description": "Could not persist the catalog",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/display/presets/{id}": {
      "put": {
        "tags": [
          "display"
        ],
        "summary": "Update a custom preset",
        "operationId": "updateCustomPreset",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "The custom preset id",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CustomPresetInput"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "Preset updated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CustomPreset"
                }
              }
            }
          },
          "400": {
            "description": "Empty name",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No custom preset with that id",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "500": {
            "description": "Could not persist the catalog",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "display"
        ],
        "summary": "Delete a custom preset",
        "description": "Removes it from the catalog. The active policy is untouched — if this preset was the one applied,\nthe running behavior stays exactly as it was (the catalog and `display-settings.json` are decoupled).",
        "operationId": "deleteCustomPreset",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "The custom preset id",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Preset deleted"
          },
          "401": {
            "description": "Missing or invalid bearer token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No custom preset with that id",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "500": {
            "description": "Could not persist the catalog",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/display/release": {
      "post": {
        "tags": [
          "display"
        ],
        "summary": "Release kept virtual displays",
        "description": "Tear down lingering/pinned displays now — so a physical-screen user gets their screen back\nwithout waiting out the linger. `slot` releases one; omit it to release all kept displays.\nActive (streaming) displays are never torn down here (that is session control).",
        "operationId": "releaseDisplay",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ReleaseDisplayRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "The number of kept displays released",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReleaseDisplayResult"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/display/settings": {
      "get": {
        "tags": [
          "display"
        ],
        "summary": "Display-management policy",
        "description": "The stored virtual-display policy (lifecycle, topology, conflict handling, identity, layout),\nevery preset's expansion, and which options this build enforces yet. See\n`design/display-management.md`.",
        "operationId": "getDisplaySettings",
        "responses": {
          "200": {
            "description": "Stored policy + preset expansions + enforced options",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DisplaySettingsState"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      },
      "put": {
        "tags": [
          "display"
        ],
        "summary": "Set the display-management policy",
        "description": "Persists a new policy (validated + clamped) and applies it from the next connect/teardown — a\nrunning session keeps the display it opened on. `keep_alive: forever` (the gaming-rig preset) is\nhonored (the display is Pinned; free it via `POST /display/release`).",
        "operationId": "setDisplaySettings",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DisplayPolicy"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "Policy stored; the new state",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DisplaySettingsState"
                }
              }
            }
          },
          "400": {
            "description": "Malformed policy body",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "500": {
            "description": "Policy could not be persisted",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/display/state": {
      "get": {
        "tags": [
          "display"
        ],
        "summary": "Live virtual displays",
        "description": "The host's managed virtual displays right now — active (streaming), lingering (kept after\ndisconnect, counting down to teardown), or pinned (kept indefinitely). See\n`design/display-management.md`.",
        "operationId": "getDisplayState",
        "responses": {
          "200": {
            "description": "The live/kept virtual displays",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DisplayStateResponse"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/events": {
      "get": {
        "tags": [
          "events"
        ],
        "summary": "Stream host lifecycle events (SSE)",
        "description": "Server-Sent Events stream of the host's lifecycle events: client connect/disconnect, session\nand stream start/end, pairing decisions, display create/release, library changes, host\nstart/stop — both protocol planes. Frames carry `id:` = the event's monotonic `seq`,\n`event:` = its kind, and `data:` = the event JSON (schema-versioned, additive-only).\n\nResume: standard `Last-Event-ID` (or `?since=`) replays from the in-memory ring; a consumer\nthat fell off the ring receives an `event: dropped` frame first and should resync via the\nREST snapshots. Keep-alive comments are sent every 15 s.",
        "operationId": "streamEvents",
        "parameters": [
          {
            "name": "since",
            "in": "query",
            "description": "Resume cursor: only events with `seq` greater than this are sent (the ring keeps the newest ~1024). `Last-Event-ID` takes precedence.",
            "required": false,
            "schema": {
              "type": "integer",
              "format": "int64",
              "minimum": 0
            }
          },
          {
            "name": "kinds",
            "in": "query",
            "description": "Comma-separated server-side kind filter: exact kinds (`pairing.pending`) or `domain.*` prefixes (`stream.*`).",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "Last-Event-ID",
            "in": "header",
            "description": "SSE auto-reconnect cursor — the `id:` of the last received frame.",
            "required": false,
            "schema": {
              "type": [
                "integer",
                "null"
              ],
              "format": "int64",
              "minimum": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "SSE stream; each frame's `data:` is one HostEvent",
            "content": {
              "text/event-stream": {
                "schema": {
                  "$ref": "#/components/schemas/HostEvent"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "503": {
            "description": "Concurrent event-stream cap reached — retry shortly",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/game/end": {
      "post": {
        "tags": [
          "session"
        ],
        "summary": "End a launched game",
        "description": "Ends a game whose session has already gone and which is waiting out its reconnect window — the\nconsole's \"End now\" for a game the host is about to close anyway. `app_id` picks one title; omit it\nto end every waiting game.\n\nThis does **not** touch a game whose session is still live: ending that is session management\n(`DELETE /session`), and how the game is treated then follows the operator's policy.",
        "operationId": "endGame",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EndGameRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "How many waiting games were ended",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EndGameResult"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "409": {
            "description": "No game is waiting to be ended",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/gpus": {
      "get": {
        "tags": [
          "gpu"
        ],
        "summary": "GPU inventory and selection",
        "description": "Lists the host's hardware GPUs, the persisted auto/manual preference, the GPU the next session\nwill use (and why), and the GPU live sessions encode on right now.",
        "operationId": "listGpus",
        "responses": {
          "200": {
            "description": "GPU inventory + selection state",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GpuState"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/gpus/preference": {
      "put": {
        "tags": [
          "gpu"
        ],
        "summary": "Set the GPU preference",
        "description": "`auto` restores automatic selection (`PUNKTFUNK_RENDER_ADAPTER` pin, else max dedicated VRAM);\n`manual` pins capture + encode to the given GPU. Persisted across restarts; applies to the\n**next** session (a running session keeps its GPU). If the preferred GPU is absent at session\nstart the host falls back to automatic selection rather than failing.",
        "operationId": "setGpuPreference",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SetGpuPreference"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "Preference stored; the new selection state",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GpuState"
                }
              }
            }
          },
          "400": {
            "description": "Unknown mode, or `gpu_id` missing / not a listed GPU",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "500": {
            "description": "Preference could not be persisted",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/health": {
      "get": {
        "tags": [
          "host"
        ],
        "summary": "Liveness probe",
        "description": "Always available without authentication.",
        "operationId": "getHealth",
        "responses": {
          "200": {
            "description": "Host is up",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Health"
                }
              }
            }
          }
        },
        "security": [
          {}
        ]
      }
    },
    "/api/v1/hooks": {
      "get": {
        "tags": [
          "hooks"
        ],
        "summary": "Get the hook configuration",
        "description": "The operator's `hooks.json`: commands and webhooks fired on host lifecycle events. Empty\nwhen unconfigured.",
        "operationId": "getHooks",
        "responses": {
          "200": {
            "description": "The stored hook configuration",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HooksConfig"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      },
      "put": {
        "tags": [
          "hooks"
        ],
        "summary": "Replace the hook configuration",
        "description": "Validates and persists a full `hooks.json` document (this is a whole-document PUT, not a\npatch). Applies from the next event — no restart. Hook commands run as the host user\n(interactive user session on Windows): treat this configuration as operator-privileged.",
        "operationId": "setHooks",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/HooksConfig"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "Configuration stored; the new state",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HooksConfig"
                }
              }
            }
          },
          "400": {
            "description": "Structurally invalid configuration",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "500": {
            "description": "Configuration could not be persisted",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/host": {
      "get": {
        "tags": [
          "host"
        ],
        "summary": "Host identity and capabilities",
        "operationId": "getHostInfo",
        "responses": {
          "200": {
            "description": "Host identity, versions, codecs, and port map",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HostInfo"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/library": {
      "get": {
        "tags": [
          "library"
        ],
        "summary": "List the game library",
        "description": "Every installed-store title (Steam, read from the host's local files — no Steam API key)\nmerged with the user's custom entries, sorted by title. Artwork fields are URLs the client\nfetches directly (the public Steam CDN for Steam titles). `?provider=` narrows to the\nentries a given external provider owns; `?platform=` to one platform (case-insensitive —\ninstalled-store titles are `PC`, custom/provider entries carry whatever was authored).",
        "operationId": "getLibrary",
        "parameters": [
          {
            "name": "provider",
            "in": "query",
            "description": "Only entries owned by this external provider",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "platform",
            "in": "query",
            "description": "Only entries on this platform (case-insensitive, e.g. `PS2`)",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Unified library across all stores",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/GameEntry"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/library/art/{id}/{kind}": {
      "get": {
        "tags": [
          "library"
        ],
        "summary": "Fetch one cover-art image for a library entry",
        "description": "Resolves `kind` (`portrait` | `hero` | `logo` | `header`) for the given library id and streams\nthe image bytes. For a Steam title, the host's own local Steam cache is tried first (exact —\nit's what the user's Steam client already shows for it), the public Steam CDN's flat URL\nconvention as a fallback (newer titles' CDN assets can live at a per-asset-hash path the host\ncan't predict, in which case this 404s and the client falls through to its next art candidate).\nOnly Steam ids are backed today; any other store 404s.",
        "operationId": "getLibraryArt",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "The store-qualified library id, e.g. `steam:570`",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "kind",
            "in": "path",
            "description": "`portrait` | `hero` | `logo` | `header`",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Image bytes",
            "content": {
              "image/jpeg": {}
            }
          },
          "401": {
            "description": "Missing or invalid credentials",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No art of that kind for that id",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/library/custom": {
      "post": {
        "tags": [
          "library"
        ],
        "summary": "Add a custom library entry",
        "description": "Creates a user-curated title (e.g. a non-Steam game, an emulator, a ROM) with caller-supplied\nartwork URLs. The host assigns a stable id, returned in the body.",
        "operationId": "createCustomGame",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CustomInput"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "description": "Entry created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CustomEntry"
                }
              }
            }
          },
          "400": {
            "description": "Empty title",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "500": {
            "description": "Could not persist the catalog",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/library/custom/{id}": {
      "put": {
        "tags": [
          "library"
        ],
        "summary": "Update a custom library entry",
        "operationId": "updateCustomGame",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "The custom entry id (without the `custom:` prefix)",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CustomInput"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "Entry updated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CustomEntry"
                }
              }
            }
          },
          "400": {
            "description": "Empty title",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No custom entry with that id",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "500": {
            "description": "Could not persist the catalog",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "library"
        ],
        "summary": "Delete a custom library entry",
        "operationId": "deleteCustomGame",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "The custom entry id (without the `custom:` prefix)",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Entry deleted"
          },
          "401": {
            "description": "Missing or invalid bearer token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No custom entry with that id",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "500": {
            "description": "Could not persist the catalog",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/library/provider/{provider}": {
      "put": {
        "tags": [
          "library"
        ],
        "summary": "Replace a provider's library entries (declarative reconcile)",
        "description": "Atomically replaces the full entry set owned by `{provider}` (RFC §8): the payload is the\nprovider's desired list, keyed by its own stable `external_id` — the host diffs, keeps each\nsurviving title's host id stable across reconciles, drops orphans, and never touches manual\nentries or other providers'. An empty array removes everything the provider owns. Emits\n`library.changed` with the provider as `source`.",
        "operationId": "reconcileProviderEntries",
        "parameters": [
          {
            "name": "provider",
            "in": "path",
            "description": "The provider id ([a-z0-9._-], `manual` reserved)",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/ProviderEntryInput"
                }
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "The provider's resulting entries (host ids assigned/kept)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/CustomEntry"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid provider id or payload",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "500": {
            "description": "Could not persist the catalog",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "library"
        ],
        "summary": "Remove a provider's library entries",
        "description": "Deletes every entry owned by `{provider}` — the clean-uninstall path for a provider plugin\n(RFC §8). Emits `library.changed` when anything was removed.",
        "operationId": "deleteProviderEntries",
        "parameters": [
          {
            "name": "provider",
            "in": "path",
            "description": "The provider id",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "How many entries were removed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProviderRemoved"
                }
              }
            }
          },
          "400": {
            "description": "Invalid provider id",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "500": {
            "description": "Could not persist the catalog",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/library/scanners": {
      "get": {
        "tags": [
          "library"
        ],
        "summary": "List the library scanners",
        "description": "The installed-store scanners this host supports — the list is platform-dependent (Steam\neverywhere; Lutris + Heroic on Linux; Epic, GOG, and Xbox/Game Pass on Windows), so the console\nrenders a toggle only for scanners that can do anything here. Scanners default to enabled;\ndisabling one hides its titles from every library surface from the next read. The user-curated\ncustom store is not a scanner and is always on.",
        "operationId": "listLibraryScanners",
        "responses": {
          "200": {
            "description": "This host's scanners with their enable state",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/ScannerInfo"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/library/scanners/{id}": {
      "put": {
        "tags": [
          "library"
        ],
        "summary": "Enable or disable a library scanner",
        "description": "Persists the toggle and applies it from the next library read (no restart). Disabling a scanner\nhides its titles everywhere — the console grid, native clients, and the GameStream app list —\nand re-enabling brings them straight back (nothing is deleted; the scan just runs again). Emits\n`library.changed` with the scanner id as `source` when the state changed.",
        "operationId": "setLibraryScanner",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "The scanner id (e.g. `steam`)",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ScannerToggle"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "Toggle stored; the full scanner list",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/ScannerInfo"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such scanner on this platform",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "500": {
            "description": "Could not persist the settings",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/local/summary": {
      "get": {
        "tags": [
          "host"
        ],
        "summary": "Local status summary for the tray icon",
        "description": "Non-sensitive status (counts and booleans only — no PIN values, no fingerprints, no device\nnames). Unauthenticated, but served to loopback peers only.",
        "operationId": "getLocalSummary",
        "responses": {
          "200": {
            "description": "Non-sensitive local host status (loopback peers only)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LocalSummary"
                }
              }
            }
          },
          "401": {
            "description": "Non-loopback peer",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        },
        "security": [
          {}
        ]
      }
    },
    "/api/v1/logs": {
      "get": {
        "tags": [
          "logs"
        ],
        "summary": "Host logs",
        "description": "The host's recent log entries — an in-memory ring of the newest few thousand, captured at\nDEBUG and above regardless of `RUST_LOG`. Follow live by polling with `after` set to the last\nresponse's `next` cursor; a `dropped: true` means entries were evicted between polls (the ring\nwrapped). Bearer-only: logs can reference client identities and host paths, so this is part of\nthe loopback-only admin surface, never the LAN-readable mTLS one.",
        "operationId": "logsGet",
        "parameters": [
          {
            "name": "after",
            "in": "query",
            "description": "Return entries with seq greater than this (omitted/0 = oldest retained)",
            "required": false,
            "schema": {
              "type": "integer",
              "format": "int64",
              "minimum": 0
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Max entries per response (default and cap 1000)",
            "required": false,
            "schema": {
              "type": "integer",
              "format": "int32",
              "minimum": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Entries after the cursor, oldest first",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LogPage"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/native/clients": {
      "get": {
        "tags": [
          "native"
        ],
        "summary": "List native paired clients",
        "operationId": "listNativeClients",
        "responses": {
          "200": {
            "description": "Paired native clients",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/NativeClient"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/native/clients/{fingerprint}": {
      "delete": {
        "tags": [
          "native"
        ],
        "summary": "Unpair a native client",
        "description": "Removes a punktfunk/1 client from the native trust store by fingerprint.",
        "operationId": "unpairNativeClient",
        "parameters": [
          {
            "name": "fingerprint",
            "in": "path",
            "description": "Hex SHA-256 of the client certificate (case-insensitive)",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Client unpaired"
          },
          "401": {
            "description": "Missing or invalid bearer token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No paired native client with that fingerprint",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "503": {
            "description": "Native host not enabled",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/native/pair": {
      "get": {
        "tags": [
          "native"
        ],
        "summary": "Native pairing status",
        "description": "The native (punktfunk/1) pairing window. Poll while armed to show the PIN + countdown.\n`enabled: false` means this host runs GameStream only (no `--native`).",
        "operationId": "getNativePairing",
        "responses": {
          "200": {
            "description": "Native pairing status",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NativePairStatus"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "native"
        ],
        "summary": "Disarm native pairing",
        "description": "Closes the pairing window immediately (no new ceremonies accepted).",
        "operationId": "disarmNativePairing",
        "responses": {
          "204": {
            "description": "Pairing disarmed"
          },
          "401": {
            "description": "Missing or invalid bearer token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "503": {
            "description": "Native host not enabled",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/native/pair/arm": {
      "post": {
        "tags": [
          "native"
        ],
        "summary": "Arm native pairing",
        "description": "Opens a pairing window and mints a fresh PIN to display. The user enters it on their device\nwithin `ttl_secs`; the device then appears in the native client list.",
        "operationId": "armNativePairing",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ArmNativePairing"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "Pairing armed; the response carries the PIN to display",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NativePairStatus"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "503": {
            "description": "Native host not available in this process",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/native/pending": {
      "get": {
        "tags": [
          "native"
        ],
        "summary": "List devices awaiting pairing approval",
        "description": "Unpaired devices that tried to connect while the host requires pairing. Approve one to pair\nit without a PIN (delegated approval); entries expire after ~10 minutes.",
        "operationId": "listPendingDevices",
        "responses": {
          "200": {
            "description": "Devices awaiting approval (empty when none, or when the native host is not enabled)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/PendingDevice"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/native/pending/{id}/approve": {
      "post": {
        "tags": [
          "native"
        ],
        "summary": "Approve a pending device",
        "description": "Pairs the device's certificate fingerprint — it can connect immediately (no PIN). Optionally\nrelabel it via the body; send `{}` to keep the name it knocked with.",
        "operationId": "approvePendingDevice",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Pending-request id from the pending list",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32",
              "minimum": 0
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ApprovePending"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "Device paired",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NativeClient"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No pending request with that id (expired?)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "500": {
            "description": "Could not persist the trust store",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "503": {
            "description": "Native host not enabled",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/native/pending/{id}/deny": {
      "post": {
        "tags": [
          "native"
        ],
        "summary": "Deny a pending device",
        "description": "Drops the request. Not a blocklist — the device's next attempt knocks again.",
        "operationId": "denyPendingDevice",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Pending-request id from the pending list",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32",
              "minimum": 0
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Request dropped"
          },
          "401": {
            "description": "Missing or invalid bearer token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No pending request with that id",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "503": {
            "description": "Native host not enabled",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/pair": {
      "get": {
        "tags": [
          "pairing"
        ],
        "summary": "Pairing-flow status",
        "description": "Poll this to know when to prompt the user for the PIN Moonlight displays.",
        "operationId": "getPairingStatus",
        "responses": {
          "200": {
            "description": "Whether a pairing handshake is waiting for a PIN",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PairingStatus"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/pair/pin": {
      "post": {
        "tags": [
          "pairing"
        ],
        "summary": "Submit the pairing PIN",
        "description": "Delivers the PIN the Moonlight client is displaying, completing the out-of-band half\nof the pairing handshake.",
        "operationId": "submitPairingPin",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SubmitPin"
              }
            }
          },
          "required": true
        },
        "responses": {
          "204": {
            "description": "PIN delivered to the waiting handshake"
          },
          "400": {
            "description": "Malformed PIN or unparseable JSON body",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "409": {
            "description": "No pairing handshake is waiting for a PIN",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "415": {
            "description": "Body is not application/json",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "422": {
            "description": "JSON body does not match the schema",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/plugins": {
      "get": {
        "tags": [
          "plugins"
        ],
        "summary": "List registered plugins",
        "description": "The live plugin directory (lease not expired), sorted by title. **Secret-free**: each entry\nreports its id, title, optional version, and — for plugins that serve one — a UI descriptor\n(loopback port + icon). The console renders these as nav entries and proxies to the port; it\nfetches the secret separately, server-side.",
        "operationId": "listPlugins",
        "responses": {
          "200": {
            "description": "Live plugin registrations",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/PluginSummary"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/plugins/{id}": {
      "put": {
        "tags": [
          "plugins"
        ],
        "summary": "Register or renew a plugin",
        "description": "Upserts the plugin's directory entry and renews its lease (TTL 90 s). Idempotent: a plugin PUTs\nthis every ~30 s while it runs. The optional `ui` block declares a loopback UI surface the console\nwill proxy and add to its nav. Emits `plugins.changed` when an operator-visible field changed\n(first registration, restart, or re-scan) — a pure renewal is silent.",
        "operationId": "registerPlugin",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "The plugin id (its `definePlugin` name: `[a-z][a-z0-9-]*`)",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PluginRegistration"
              }
            }
          },
          "required": true
        },
        "responses": {
          "204": {
            "description": "Registered / renewed"
          },
          "400": {
            "description": "Invalid id or registration",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "plugins"
        ],
        "summary": "Deregister a plugin",
        "description": "The clean-shutdown path: removes the plugin's directory entry immediately (the SDK helper calls\nthis from its scope finalizer on `SIGTERM`). Emits `plugins.changed` when a live entry was\nremoved. Idempotent — deleting an unknown/expired id is a no-op `204`.",
        "operationId": "deregisterPlugin",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "The plugin id",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Deregistered (or already absent)"
          },
          "401": {
            "description": "Missing or invalid bearer token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/plugins/{id}/ui-credential": {
      "get": {
        "tags": [
          "plugins"
        ],
        "summary": "Fetch a plugin UI's proxy credential",
        "description": "Returns `{port, secret}` for a live plugin's loopback UI — the console proxy's server-side lookup.\nBearer + loopback only (like every mutation), and additionally excluded from the console's browser\npassthrough: the secret never reaches a browser.",
        "operationId": "getPluginUiCredential",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "The plugin id",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The proxy credential",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UiCredential"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No live plugin with that id, or it serves no UI",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/session": {
      "delete": {
        "tags": [
          "session"
        ],
        "summary": "Stop the active session",
        "description": "Kicks the connected client: stops the video/audio stream threads and clears the launch\nstate. Idempotent — succeeds even when nothing is streaming.\n\nCounts as a **deliberate** stop, exactly like a client pressing Stop: the display skips its\nkeep-alive linger, and the end-game-on-session-end policy (if the operator enabled one) applies.",
        "operationId": "stopSession",
        "responses": {
          "204": {
            "description": "Session stopped (or none was active)"
          },
          "401": {
            "description": "Missing or invalid bearer token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/session/idr": {
      "post": {
        "tags": [
          "session"
        ],
        "summary": "Force a keyframe",
        "description": "Asks the encoder for an IDR frame on the active video stream (what a client requests\nafter unrecoverable loss — exposed for debugging).",
        "operationId": "requestIdr",
        "responses": {
          "202": {
            "description": "Keyframe requested"
          },
          "401": {
            "description": "Missing or invalid bearer token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "409": {
            "description": "No active video stream",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/session/settings": {
      "get": {
        "tags": [
          "session"
        ],
        "summary": "Session⇄game lifetime settings",
        "description": "Whether a launched game's exit ends the streaming session, and whether a session ending ends the\ngame (with the reconnect window that protects a dropped client's unsaved progress). See\n`design/session-game-lifetime.md`.",
        "operationId": "getSessionSettings",
        "responses": {
          "200": {
            "description": "Stored settings + which axes this build enforces",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SessionSettingsState"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      },
      "put": {
        "tags": [
          "session"
        ],
        "summary": "Set the session⇄game lifetime settings",
        "description": "Persists the settings (clamped) and applies them from the next decision — including to a session\nthat is already streaming, since the policy is read when a session ends rather than when it starts.",
        "operationId": "setSessionSettings",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SessionSettings"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "Settings stored; the new state",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SessionSettingsState"
                }
              }
            }
          },
          "400": {
            "description": "Malformed settings body",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "500": {
            "description": "Settings could not be persisted",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/stats/capture/live": {
      "get": {
        "tags": [
          "stats"
        ],
        "summary": "Live in-progress capture",
        "description": "The full sample time-series of the capture currently recording, for live graphing. `404` when\nnothing is armed.",
        "operationId": "statsCaptureLive",
        "responses": {
          "200": {
            "description": "The in-progress capture (meta + samples so far)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Capture"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No capture is currently recording",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/stats/capture/start": {
      "post": {
        "tags": [
          "stats"
        ],
        "summary": "Start a stats capture",
        "description": "Arms a new performance-stats capture. Idempotent: if a capture is already running this returns\nthe current status unchanged. While armed, the streaming loops emit aggregated samples (~ every\n1–2 s) into the in-progress capture, readable live via `GET /stats/capture/live`.",
        "operationId": "statsCaptureStart",
        "responses": {
          "200": {
            "description": "Capture armed (or already running)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StatsStatus"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/stats/capture/status": {
      "get": {
        "tags": [
          "stats"
        ],
        "summary": "Stats capture status",
        "description": "Whether a capture is armed, its sample count, and start time. Poll this (e.g. every 2 s) to\ndrive the capture-control UI.",
        "operationId": "statsCaptureStatus",
        "responses": {
          "200": {
            "description": "In-progress capture status (idle when not armed)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StatsStatus"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/stats/capture/stop": {
      "post": {
        "tags": [
          "stats"
        ],
        "summary": "Stop the stats capture",
        "description": "Disarms the in-progress capture and writes it to disk atomically, returning its summary. If\nnothing was recording, returns `204 No Content`.",
        "operationId": "statsCaptureStop",
        "responses": {
          "200": {
            "description": "Capture stopped and saved",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CaptureMeta"
                }
              }
            }
          },
          "204": {
            "description": "Nothing was recording"
          },
          "401": {
            "description": "Missing or invalid bearer token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "500": {
            "description": "Could not write the recording to disk",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/stats/recordings": {
      "get": {
        "tags": [
          "stats"
        ],
        "summary": "List saved recordings",
        "description": "Every saved capture's summary (the `meta` head only — not the sample body), newest first.",
        "operationId": "statsRecordingsList",
        "responses": {
          "200": {
            "description": "Saved capture summaries, newest first",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/CaptureMeta"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/stats/recordings/{id}": {
      "get": {
        "tags": [
          "stats"
        ],
        "summary": "Get a saved recording",
        "description": "The full capture (meta + samples) for `id`, for graphing or download.",
        "operationId": "statsRecordingGet",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "The recording id (its filename stem)",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The full capture",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Capture"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No recording with that id",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "500": {
            "description": "The recording file is unreadable",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "stats"
        ],
        "summary": "Delete a saved recording",
        "description": "Removes the recording `id` from disk. `404` if there is no such recording.",
        "operationId": "statsRecordingDelete",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "The recording id (its filename stem)",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Recording deleted"
          },
          "401": {
            "description": "Missing or invalid bearer token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No recording with that id",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "500": {
            "description": "Could not delete the recording",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/status": {
      "get": {
        "tags": [
          "host"
        ],
        "summary": "Live host status",
        "operationId": "getStatus",
        "responses": {
          "200": {
            "description": "Streaming/pairing state and the active session, if any",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RuntimeStatus"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/store/catalog": {
      "get": {
        "tags": [
          "store"
        ],
        "summary": "Browse the plugin catalog",
        "description": "The merged shelf across every configured source, annotated with what this host already has and\nwhat it can run. Sources past their freshness window are refreshed first; a source that can't be\nreached keeps serving its last good copy, marked `stale` (a LAN-only host still has a working\nstore — an entry's pin travelled with the entry).",
        "operationId": "getPluginCatalog",
        "responses": {
          "200": {
            "description": "The merged catalog",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CatalogResponse"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "Not authorized for the plugin token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/store/install": {
      "post": {
        "tags": [
          "store"
        ],
        "summary": "Install a plugin",
        "description": "Either `{source, id}` — a catalogued entry, installed at its pinned version after its integrity\nis re-checked against the registry — or `{spec, accept_unverified: true}`, which installs an\nunreviewed package the operator takes responsibility for. Returns `202` with a job id; watch it\nat `GET /store/jobs/{id}`.\n\nOne package operation runs at a time (`409` otherwise): `bun` operations share a lockfile and a\n`node_modules` tree.",
        "operationId": "installPlugin",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/InstallRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "202": {
            "description": "Install job started",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobRef"
                }
              }
            }
          },
          "400": {
            "description": "Unknown entry, bad spec, or missing acknowledgement",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "Not authorized for the plugin token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "409": {
            "description": "Another package operation is in flight",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/store/installed": {
      "get": {
        "tags": [
          "store"
        ],
        "summary": "List installed plugins",
        "description": "What's actually in the plugins directory, joined with how it got there (the provenance manifest)\nand whether it is registered right now. A package with no provenance record was installed with\nthe CLI and reports `tier: \"cli\"` — absence is the answer, not a gap.",
        "operationId": "listInstalledPlugins",
        "responses": {
          "200": {
            "description": "Installed plugin packages",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/InstalledView"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "Not authorized for the plugin token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/store/jobs": {
      "get": {
        "tags": [
          "store"
        ],
        "summary": "List recent package jobs",
        "operationId": "listPluginJobs",
        "responses": {
          "200": {
            "description": "Recent install/uninstall jobs, oldest first",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Job"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "Not authorized for the plugin token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/store/jobs/{id}": {
      "get": {
        "tags": [
          "store"
        ],
        "summary": "Follow one package job",
        "description": "Poll this while `state` is `running`; `log` carries the tail of the package manager's output.",
        "operationId": "getPluginJob",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "The job id returned by install/uninstall",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The job",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Job"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "Not authorized for the plugin token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such job (they are kept for a bounded history)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/store/refresh": {
      "post": {
        "tags": [
          "store"
        ],
        "summary": "Refresh every catalog now",
        "description": "Bypasses the freshness window and re-fetches all sources, then returns the merged catalog.",
        "operationId": "refreshPluginCatalog",
        "responses": {
          "200": {
            "description": "The freshly-fetched catalog",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CatalogResponse"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "Not authorized for the plugin token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/store/runtime": {
      "get": {
        "tags": [
          "store"
        ],
        "summary": "Plugin runner state",
        "description": "Installed plugins only run while the runner is on, and the runner discovers units at startup —\nso this is both the \"is anything running\" answer and the explanation for a freshly installed\nplugin that hasn't appeared yet.",
        "operationId": "getPluginRuntime",
        "responses": {
          "200": {
            "description": "Runner state",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RuntimeView"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "Not authorized for the plugin token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "store"
        ],
        "summary": "Turn the plugin runner on or off",
        "operationId": "setPluginRuntime",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RuntimeRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "The resulting runner state",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RuntimeView"
                }
              }
            }
          },
          "400": {
            "description": "The runner could not be switched",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "Not authorized for the plugin token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/store/sources": {
      "get": {
        "tags": [
          "store"
        ],
        "summary": "List catalog sources",
        "operationId": "listPluginSources",
        "responses": {
          "200": {
            "description": "Configured sources, built-in first",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/SourceView"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "Not authorized for the plugin token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/store/sources/{name}": {
      "put": {
        "tags": [
          "store"
        ],
        "summary": "Add or update a catalog source",
        "description": "Adding a source is a trust decision: its entries become installable on this host. They are\nattributed to it in the console and never carry the \"verified\" badge, which belongs to the\nbuilt-in source alone.",
        "operationId": "putPluginSource",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "description": "Source slug (`[a-z][a-z0-9-]*`)",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SourceInput"
              }
            }
          },
          "required": true
        },
        "responses": {
          "204": {
            "description": "Source saved"
          },
          "400": {
            "description": "Invalid name, url or key — or the reserved built-in name",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "Not authorized for the plugin token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "store"
        ],
        "summary": "Remove a catalog source",
        "operationId": "deletePluginSource",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "description": "Source slug",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Removed (or already absent)"
          },
          "401": {
            "description": "Missing or invalid bearer token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "The built-in source cannot be removed, or the plugin token is not authorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/store/uninstall": {
      "post": {
        "tags": [
          "store"
        ],
        "summary": "Uninstall a plugin",
        "description": "Removes the package and forgets its provenance, then restarts the runner. Only names the runner\nwould actually supervise are accepted, so this can't be used to rip a shared dependency out of\nthe tree.",
        "operationId": "uninstallPlugin",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UninstallRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "202": {
            "description": "Uninstall job started",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobRef"
                }
              }
            }
          },
          "400": {
            "description": "Not a plugin package name",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "Not authorized for the plugin token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "409": {
            "description": "Another package operation is in flight",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "ActiveGame": {
        "type": "object",
        "description": "One launched game, for the console's running-game card.",
        "required": [
          "client",
          "title",
          "plane",
          "state"
        ],
        "properties": {
          "app_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "Store-qualified library id (`steam:570`) — the key the console matches against `GET /library`\nto show box art. Absent for an operator-typed GameStream command."
          },
          "client": {
            "type": "string",
            "description": "Client-supplied device name of the session that launched it; may be empty."
          },
          "grace_remaining_s": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64",
            "description": "Seconds until this game is ended — only present on a `grace` row.",
            "minimum": 0
          },
          "plane": {
            "$ref": "#/components/schemas/Plane",
            "description": "`native` or `gamestream`."
          },
          "session_id": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64",
            "description": "The session streaming it; `null` for a game waiting out its reconnect window.",
            "minimum": 0
          },
          "state": {
            "type": "string",
            "description": "`launching` (launched, not seen running yet), `running`, `exited`, or `grace` (its session is\ngone and it will be ended when the reconnect window closes).",
            "example": "running"
          },
          "store": {
            "type": [
              "string",
              "null"
            ],
            "description": "Which store surfaced it (`steam`, `heroic`, `custom`, …), when known."
          },
          "title": {
            "type": "string",
            "description": "Display title."
          }
        }
      },
      "ApiActiveGpu": {
        "type": "object",
        "description": "The GPU live sessions are encoding on right now.",
        "required": [
          "id",
          "name",
          "vendor",
          "backend",
          "sessions"
        ],
        "properties": {
          "backend": {
            "type": "string",
            "description": "The encode backend in use (`nvenc` | `amf` | `qsv` | `vaapi` | `software`)."
          },
          "id": {
            "type": "string",
            "description": "Stable id matching an entry of `gpus` (empty for the CPU/software encoder)."
          },
          "name": {
            "type": "string"
          },
          "sessions": {
            "type": "integer",
            "format": "int32",
            "description": "Number of live encode sessions on it.",
            "minimum": 0
          },
          "vendor": {
            "type": "string",
            "description": "`nvidia` | `amd` | `intel` | `other`."
          }
        }
      },
      "ApiCodec": {
        "type": "string",
        "description": "Video codec identifier. The wire token matches the codec's canonical name used across the\nstack (SDP/GameStream advertisement, the stats-capture `CaptureMeta.codec`, and the encoder's\n[`Codec::label`]) — notably `H.265` serializes as `\"hevc\"`, not `\"h265\"`, so the same codec\nreads identically on every console page.",
        "enum": [
          "h264",
          "hevc",
          "av1",
          "pyrowave"
        ]
      },
      "ApiDisplayInfo": {
        "type": "object",
        "description": "One live or kept virtual display.",
        "required": [
          "slot",
          "backend",
          "mode",
          "state",
          "sessions",
          "group",
          "display_index",
          "x",
          "y",
          "topology"
        ],
        "properties": {
          "backend": {
            "type": "string",
            "description": "Backend name (`pf-vdisplay`, `kwin`, …)."
          },
          "client": {
            "type": [
              "string",
              "null"
            ],
            "description": "Short client label, when the owner tracks it."
          },
          "display_index": {
            "type": "integer",
            "format": "int32",
            "description": "This display's ordinal within its group, in acquire order (0-based).",
            "minimum": 0
          },
          "expires_in_ms": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64",
            "description": "Milliseconds until a lingering display is torn down (absent when active/pinned).",
            "minimum": 0
          },
          "group": {
            "type": "integer",
            "format": "int32",
            "description": "Display group (shared desktop) id — several displays with the same group form one desktop (§6A).",
            "minimum": 0
          },
          "identity_slot": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int32",
            "description": "Stable per-client identity slot keying persistent config + manual layout (absent = shared/anonymous).",
            "minimum": 0
          },
          "mode": {
            "type": "string",
            "description": "`WIDTHxHEIGHT@HZ`."
          },
          "sessions": {
            "type": "integer",
            "format": "int32",
            "description": "Live sessions holding the display.",
            "minimum": 0
          },
          "slot": {
            "type": "integer",
            "format": "int64",
            "description": "Stable-enough id for the `/display/release` `slot` argument.",
            "minimum": 0
          },
          "state": {
            "type": "string",
            "description": "`active` | `lingering` | `pinned`."
          },
          "topology": {
            "type": "string",
            "description": "Effective topology for this display's group (`extend` | `primary` | `exclusive`)."
          },
          "x": {
            "type": "integer",
            "format": "int32",
            "description": "Desktop-space top-left `x` (auto-row or the console's manual arrangement, §6.2)."
          },
          "y": {
            "type": "integer",
            "format": "int32",
            "description": "Desktop-space top-left `y`."
          }
        }
      },
      "ApiError": {
        "type": "object",
        "description": "Error envelope for every non-2xx response.",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string"
          }
        }
      },
      "ApiGpu": {
        "type": "object",
        "description": "One hardware GPU on the host (software/WARP adapters are never listed).",
        "required": [
          "id",
          "name",
          "vendor",
          "vram_mb"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Stable identifier (`vendorid-deviceid-occurrence`, hex PCI ids) — pass to `setGpuPreference`.\nStable across reboots and driver updates, unlike an adapter index or LUID.",
            "example": "10de-2c05-0"
          },
          "name": {
            "type": "string",
            "description": "Adapter/marketing name.",
            "example": "NVIDIA GeForce RTX 5070 Ti"
          },
          "vendor": {
            "type": "string",
            "description": "`nvidia` | `amd` | `intel` | `other`."
          },
          "vram_mb": {
            "type": "integer",
            "format": "int64",
            "description": "Dedicated VRAM in MiB (0 where the platform doesn't expose it).",
            "minimum": 0
          }
        }
      },
      "ApiSelectedGpu": {
        "type": "object",
        "description": "The GPU the **next** session's pipeline will be created on, and why. (A preference change\napplies to the next session; a running session keeps the GPU it opened on.)",
        "required": [
          "id",
          "name",
          "vendor",
          "source"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "source": {
            "type": "string",
            "description": "Why this GPU was selected: `preference` (the manual choice), `env`\n(`PUNKTFUNK_RENDER_ADAPTER`), `auto` (max dedicated VRAM / platform default), or\n`preference_missing` (a manual choice is set but that GPU is absent — auto-selected\ninstead so the host keeps streaming)."
          },
          "vendor": {
            "type": "string",
            "description": "`nvidia` | `amd` | `intel` | `other`."
          }
        }
      },
      "ApprovePending": {
        "type": "object",
        "description": "Approve-pending-device request body. Send `{}` to keep the device's own name.",
        "properties": {
          "name": {
            "type": [
              "string",
              "null"
            ],
            "description": "Operator-chosen label for the device (defaults to the name it knocked with).",
            "example": "Living Room TV"
          }
        }
      },
      "ArmNativePairing": {
        "type": "object",
        "description": "Arm-native-pairing request body.",
        "properties": {
          "fingerprint": {
            "type": [
              "string",
              "null"
            ],
            "description": "Optional: bind the window to ONE device fingerprint (hex SHA-256, e.g. from a pending knock).\nWhen set, only a pairing attempt from that fingerprint consumes the window — so an unpaired\nLAN peer can neither pair nor burn a window armed for a specific device (security-review #9).\nOmit for an unbound window (any device may use the PIN — trusted-LAN only).",
            "example": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
          },
          "ttl_secs": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int32",
            "description": "Window length in seconds (default 120; clamped to 15–600).",
            "example": 120,
            "minimum": 0
          }
        }
      },
      "Artwork": {
        "type": "object",
        "description": "Cover art for a title. All fields are URLs (the Steam CDN for Steam titles, user-supplied for\ncustom). The client prefers `portrait` for a grid and falls back to `header` when a title has\nno 600×900 capsule (common for older Steam apps).",
        "properties": {
          "header": {
            "type": [
              "string",
              "null"
            ],
            "description": "Horizontal header (Steam `header.jpg`) — the universal fallback."
          },
          "hero": {
            "type": [
              "string",
              "null"
            ],
            "description": "Wide background (Steam `library_hero.jpg`)."
          },
          "logo": {
            "type": [
              "string",
              "null"
            ],
            "description": "Transparent title logo (Steam `logo.png`)."
          },
          "portrait": {
            "type": [
              "string",
              "null"
            ],
            "description": "Vertical capsule / poster (Steam `library_600x900.jpg`). Best for a grid."
          }
        }
      },
      "AvailableCompositor": {
        "type": "object",
        "description": "A compositor backend the host can drive a virtual output on, and whether it's usable now.",
        "required": [
          "id",
          "label",
          "available",
          "default"
        ],
        "properties": {
          "available": {
            "type": "boolean",
            "description": "Usable on this host right now: the live session's own compositor, or gamescope wherever\nits binary is installed."
          },
          "default": {
            "type": "boolean",
            "description": "True for the backend an `Auto` (unspecified) request resolves to right now."
          },
          "id": {
            "type": "string",
            "description": "Stable identifier (`\"kwin\"` | `\"wlroots\"` | `\"mutter\"` | `\"gamescope\"`) — pass this to a\nclient's `--compositor` flag."
          },
          "label": {
            "type": "string",
            "description": "Human-readable label for UIs."
          }
        }
      },
      "Capture": {
        "type": "object",
        "description": "A full capture: summary + the sample time-series. The wire + on-disk shape.",
        "required": [
          "meta",
          "samples"
        ],
        "properties": {
          "meta": {
            "$ref": "#/components/schemas/CaptureMeta"
          },
          "samples": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/StatsSample"
            }
          }
        }
      },
      "CaptureMeta": {
        "type": "object",
        "description": "Capture summary — the filename stem plus the negotiated mode/codec/client. Stored at the head\nof each on-disk recording and listed standalone (without the sample body) by\n[`StatsRecorder::list`].",
        "required": [
          "id",
          "started_unix_ms",
          "duration_ms",
          "kind",
          "width",
          "height",
          "fps",
          "codec",
          "client",
          "sample_count"
        ],
        "properties": {
          "client": {
            "type": "string",
            "description": "Short label / fingerprint prefix, or `\"\"` if unknown."
          },
          "codec": {
            "type": "string",
            "description": "`\"h264\" | \"hevc\" | \"av1\"`."
          },
          "duration_ms": {
            "type": "integer",
            "format": "int64",
            "minimum": 0
          },
          "fps": {
            "type": "integer",
            "format": "int32",
            "minimum": 0
          },
          "height": {
            "type": "integer",
            "format": "int32",
            "minimum": 0
          },
          "id": {
            "type": "string",
            "description": "e.g. `\"2026-06-26T20-14-03Z_5120x1440\"` — also the filename stem."
          },
          "kind": {
            "type": "string",
            "description": "`\"native\" | \"gamestream\"`."
          },
          "sample_count": {
            "type": "integer",
            "format": "int32",
            "minimum": 0
          },
          "started_unix_ms": {
            "type": "integer",
            "format": "int64",
            "minimum": 0
          },
          "width": {
            "type": "integer",
            "format": "int32",
            "minimum": 0
          }
        }
      },
      "CatalogEntry": {
        "type": "object",
        "description": "One row on the shelf.",
        "required": [
          "id",
          "pkg",
          "title",
          "description",
          "author",
          "version",
          "source",
          "tier",
          "platforms",
          "compatible",
          "update_available"
        ],
        "properties": {
          "author": {
            "type": "string"
          },
          "blocked": {
            "type": [
              "string",
              "null"
            ],
            "description": "A revocation covering the catalogued version — do not offer this without shouting."
          },
          "compatible": {
            "type": "boolean",
            "description": "Can this host install it?"
          },
          "description": {
            "type": "string"
          },
          "homepage": {
            "type": [
              "string",
              "null"
            ]
          },
          "icon": {
            "type": [
              "string",
              "null"
            ]
          },
          "id": {
            "type": "string"
          },
          "incompatible_reason": {
            "type": [
              "string",
              "null"
            ]
          },
          "installed_version": {
            "type": [
              "string",
              "null"
            ],
            "description": "The version installed right now, if any."
          },
          "license": {
            "type": [
              "string",
              "null"
            ]
          },
          "min_host": {
            "type": [
              "string",
              "null"
            ]
          },
          "pkg": {
            "type": "string"
          },
          "platforms": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "reviewed_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "When unom reviewed this exact tarball (built-in source only)."
          },
          "source": {
            "type": "string",
            "description": "Which source listed it."
          },
          "tier": {
            "type": "string",
            "description": "`verified` (built-in source) or `external` (an operator-added source). Never `unverified`:\nunverified installs come from a raw spec and are never listed (D7)."
          },
          "title": {
            "type": "string"
          },
          "update_available": {
            "type": "boolean",
            "description": "Installed, but at a different version than the catalog pins."
          },
          "version": {
            "type": "string",
            "description": "The one installable version this entry pins."
          }
        }
      },
      "CatalogResponse": {
        "type": "object",
        "required": [
          "host",
          "sources",
          "plugins",
          "busy"
        ],
        "properties": {
          "busy": {
            "type": "boolean",
            "description": "True while a package operation is in flight — the console disables install buttons."
          },
          "host": {
            "$ref": "#/components/schemas/HostFacts"
          },
          "plugins": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/CatalogEntry"
            }
          },
          "sources": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SourceView"
            }
          }
        }
      },
      "ClientRef": {
        "type": "object",
        "description": "The connecting/disconnecting client's identity.",
        "required": [
          "name",
          "plane"
        ],
        "properties": {
          "fingerprint": {
            "type": [
              "string",
              "null"
            ],
            "description": "Hex SHA-256 certificate fingerprint, when the client presented one."
          },
          "name": {
            "type": "string",
            "description": "Client-supplied device name; may be empty (an anonymous or compat-plane client)."
          },
          "plane": {
            "$ref": "#/components/schemas/Plane"
          }
        }
      },
      "CustomEntry": {
        "allOf": [
          {
            "$ref": "#/components/schemas/GameMeta",
            "description": "Descriptive metadata (platform, description, …), flattened — see [`GameMeta`]."
          },
          {
            "type": "object",
            "required": [
              "id",
              "title"
            ],
            "properties": {
              "art": {
                "$ref": "#/components/schemas/Artwork"
              },
              "detect": {
                "$ref": "#/components/schemas/DetectHint",
                "description": "How to recognize this title's process once it is running (design §9) — the one thing a\nprovider knows that the host cannot work out for itself.\n\nOptional: without it the entry is still tracked by the child the host spawns for it, which\ncovers every command that stays in the foreground. It earns its keep for a command that hands\noff and exits — a launcher script, a `flatpak run`, a front-end that starts an emulator — where\nthe host would otherwise lose the game the moment the shim returns."
              },
              "external_id": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The provider's own stable key for this title — the reconcile diff key, so the\nhost-assigned `id` stays stable across reconciles. Present iff `provider` is."
              },
              "id": {
                "type": "string",
                "description": "Host-assigned, stable for the life of the entry (the `{id}` in the CRUD path)."
              },
              "launch": {
                "oneOf": [
                  {
                    "type": "null"
                  },
                  {
                    "$ref": "#/components/schemas/LaunchSpec"
                  }
                ]
              },
              "prep": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/PrepCmd"
                },
                "description": "Per-title prep/undo steps (RFC §6): each `do` runs before this title launches, each\n`undo` at session end in reverse order (see [`crate::hooks::run_prep`])."
              },
              "provider": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The external provider owning this entry (RFC §8), set ONLY by the provider reconcile\nAPI — `None` = a manual entry, which no provider operation ever touches, and which the\nmanual CRUD alone may edit (the converse holds too: manual CRUD refuses provider-owned\nentries, so ownership is never ambiguous)."
              },
              "title": {
                "type": "string"
              }
            }
          }
        ],
        "description": "A user-added title, persisted in the hardened host config dir's `library.json` (see\n[`custom_path`]). Same shape the API returns and the web console edits."
      },
      "CustomInput": {
        "allOf": [
          {
            "$ref": "#/components/schemas/GameMeta",
            "description": "Descriptive metadata (platform, description, …), flattened — see [`GameMeta`]. Replaced\nwholesale on update, like `art`: an edit must round-trip every field it wants kept."
          },
          {
            "type": "object",
            "required": [
              "title"
            ],
            "properties": {
              "art": {
                "$ref": "#/components/schemas/Artwork"
              },
              "detect": {
                "$ref": "#/components/schemas/DetectHint",
                "description": "How to recognize this title's process — see [`CustomEntry::detect`]."
              },
              "launch": {
                "oneOf": [
                  {
                    "type": "null"
                  },
                  {
                    "$ref": "#/components/schemas/LaunchSpec"
                  }
                ]
              },
              "prep": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/PrepCmd"
                },
                "description": "Per-title prep/undo steps — commands run as the host user; operator-privileged config."
              },
              "title": {
                "type": "string"
              }
            }
          }
        ],
        "description": "Request body to create or replace a custom entry (no `id` — the host owns it)."
      },
      "CustomPreset": {
        "type": "object",
        "description": "A user-defined named preset: a saved bundle of the six display-behavior axes (exactly what a\nbuilt-in [`Preset`] expands to) plus the orthogonal game-session axis, that the operator names\nand applies from the console.\n\nUnlike the built-in [`Preset`]s (a closed enum), custom presets are **data** — a catalog stored in\n`<config>/display-presets.json`. Applying one writes a `Custom` [`DisplayPolicy`] carrying these\nfields (the console reuses `PUT /display/settings`), so [`DisplayPolicy::effective`] stays pure and\nthe built-in set is never touched. The catalog is decoupled from the active `display-settings.json`:\nediting or deleting a preset never mutates the running policy (re-apply to adopt a change).",
        "required": [
          "id",
          "name",
          "fields"
        ],
        "properties": {
          "fields": {
            "$ref": "#/components/schemas/EffectivePolicy",
            "description": "The six display-behavior axes this preset applies (the same shape a built-in preset expands to)."
          },
          "game_session": {
            "$ref": "#/components/schemas/GameSession",
            "description": "The game-session routing this preset applies (orthogonal to the six axes; see [`GameSession`]).\nA custom preset captures the operator's *full* setup, so — unlike a built-in preset — applying\none does set this axis."
          },
          "id": {
            "type": "string",
            "description": "Host-assigned, stable for the life of the entry (the `{id}` in the CRUD path)."
          },
          "name": {
            "type": "string",
            "description": "User-facing name shown on the preset card; editable."
          }
        }
      },
      "CustomPresetInput": {
        "type": "object",
        "description": "Request body to create or replace a custom preset (no `id` — the host owns it).",
        "required": [
          "name",
          "fields"
        ],
        "properties": {
          "fields": {
            "$ref": "#/components/schemas/EffectivePolicy"
          },
          "game_session": {
            "$ref": "#/components/schemas/GameSession"
          },
          "name": {
            "type": "string"
          }
        }
      },
      "DetectHint": {
        "type": "object",
        "description": "What an operator (or a provider plugin) can tell the host about recognizing a title — the wire\nhalf of [`DetectSpec`], and the only part of it that is ever accepted from outside.\n\nDeliberately a **subset**: the store-derived signals (a Steam appid, a launcher's environment\nmarker) are things the host discovers for itself and would be meaningless — or dangerous — to take\non someone's word. What is left is what a provider genuinely knows and the host cannot guess: where\nthe title is installed, which executable is the game, what the process is called. All three are\noptional; supplying none is the same as supplying no hint at all.\n\nNever returned by the catalog API — see the module docs on why detect data does not cross the wire\noutbound.",
        "properties": {
          "exe": {
            "type": [
              "string",
              "null"
            ],
            "description": "The game's own executable, as an absolute path."
          },
          "install_dir": {
            "type": [
              "string",
              "null"
            ],
            "description": "Where the title is installed. Any process running from under this directory is part of the\ngame — the universal recipe, and the one worth supplying if you supply only one."
          },
          "process_name": {
            "type": [
              "string",
              "null"
            ],
            "description": "The executable's file name (`Hades.exe`), when its location isn't fixed. Weakest of the three\n— see [`DetectSpec::process_name`]."
          }
        }
      },
      "DeviceRef": {
        "type": "object",
        "description": "A device in the pairing flow.",
        "required": [
          "name",
          "fingerprint",
          "plane"
        ],
        "properties": {
          "fingerprint": {
            "type": "string",
            "description": "Hex certificate fingerprint."
          },
          "name": {
            "type": "string",
            "description": "Sanitized device name (the pairing store's copy)."
          },
          "plane": {
            "$ref": "#/components/schemas/Plane"
          }
        }
      },
      "DisconnectReason": {
        "type": "string",
        "description": "Why a client went away. `Quit` is a deliberate user \"stop\" (the typed close code);\n`Timeout` is a transport idle timeout (the client vanished); `Error` is everything else.",
        "enum": [
          "quit",
          "timeout",
          "error"
        ]
      },
      "DisplayLayoutRequest": {
        "type": "object",
        "description": "Request body for `setDisplayLayout`: per-identity-slot desktop offsets, keyed by the identity-slot\nid as a string (the same id `/display/state` reports as `identity_slot`).",
        "properties": {
          "positions": {
            "type": "object",
            "description": "`{\"<identity_slot>\": {\"x\": …, \"y\": …}}` — where each arranged display's top-left sits.",
            "additionalProperties": {
              "$ref": "#/components/schemas/Position"
            },
            "propertyNames": {
              "type": "string"
            }
          }
        }
      },
      "DisplayPolicy": {
        "type": "object",
        "description": "The user-facing display-management policy — what `display-settings.json` holds and what the mgmt\nAPI GETs/PUTs. When [`preset`](Self::preset) is not [`Preset::Custom`] the explicit fields are\nignored (the console writes one or the other); [`effective`](Self::effective) resolves both to a\nsingle [`EffectivePolicy`].",
        "properties": {
          "ddc_power_off": {
            "type": "boolean",
            "description": "EXPERIMENTAL (Windows): command physical monitors' panels off over DDC/CI (VCP 0xD6 →\nDPMS off) right before an `Exclusive` isolate deactivates them, and back on at restore.\nTargets the \"connected-but-dark head\" periodic-stutter class (monitor standby\nauto-input-scan / DP link churn while the virtual display is the sole active display) at\nthe monitor-firmware level. Best-effort — monitors without DDC/CI (or with it disabled in\nthe OSD) are skipped. Orthogonal to `preset` (like `game_session`): preserved across\npreset changes; `#[serde(default)]` = off so existing `display-settings.json` files are\nuntouched."
          },
          "game_session": {
            "$ref": "#/components/schemas/GameSession",
            "description": "How a game-launching session is served (`design/gamemode-and-dedicated-sessions.md` §5.2).\nOrthogonal to `preset`/lifecycle — preserved across preset changes; `#[serde(default)]` = `Auto`\nso existing `display-settings.json` files are untouched."
          },
          "identity": {
            "$ref": "#/components/schemas/Identity"
          },
          "keep_alive": {
            "$ref": "#/components/schemas/KeepAlive"
          },
          "layout": {
            "$ref": "#/components/schemas/Layout"
          },
          "max_displays": {
            "type": "integer",
            "format": "int32",
            "description": "Upper bound on simultaneously-live virtual displays (clamped to `1..=16` on write).",
            "minimum": 0
          },
          "mode_conflict": {
            "$ref": "#/components/schemas/ModeConflict"
          },
          "pnp_disable_monitors": {
            "type": "boolean",
            "description": "EXPERIMENTAL (Windows): DISABLE physical monitors' PnP device nodes for the stream's\nduration (persistently, so a standby monitor/TV whose hot-plug events re-arrive stays\ndisabled) and re-enable them at teardown. Two selectors: the monitors an `Exclusive`\nisolate deactivated, plus — in ANY topology — external monitors that are connected but not\npart of the desktop (the standby TV that was never active, whose input auto-scan /\ninstant-on HPD cycling re-probes the link every few seconds). Targets the same\n\"connected-but-dark head\" periodic-stutter class as [`Self::ddc_power_off`], but at the\nWindows-reaction level: a disabled devnode's wake events trigger no PnP arrival, no CCD\nre-evaluation, no DWM invalidation. A crash-recovery journal re-enables leftovers on host\nstartup. Orthogonal to `preset` (like `game_session`); `#[serde(default)]` = off."
          },
          "preset": {
            "$ref": "#/components/schemas/Preset"
          },
          "topology": {
            "$ref": "#/components/schemas/Topology"
          },
          "version": {
            "type": "integer",
            "format": "int32",
            "description": "Schema version (currently 1) — lets a future field addition migrate rather than reject.",
            "minimum": 0
          }
        }
      },
      "DisplaySettingsState": {
        "type": "object",
        "description": "Full display-management state for the console: the stored policy, every preset's expansion, the\nresolved effective policy, and which options this build actually enforces yet (Stage 0 wires\nkeep-alive linger + topology; the rest are stored but not yet acted on).",
        "required": [
          "settings",
          "configured",
          "effective",
          "presets",
          "custom_presets",
          "enforced"
        ],
        "properties": {
          "configured": {
            "type": "boolean",
            "description": "True once a `display-settings.json` exists (the console has configured this host)."
          },
          "custom_presets": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/CustomPreset"
            },
            "description": "The operator's saved custom presets (`display-presets.json`) — named field-bundles rendered\nalongside the built-ins. Managed via `POST/PUT/DELETE /display/presets`; applied by writing a\n`Custom` policy carrying the preset's fields."
          },
          "effective": {
            "$ref": "#/components/schemas/EffectivePolicy",
            "description": "The effective (preset-expanded) policy currently in force."
          },
          "enforced": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Option names this build enforces right now. All five axes are now acted on (keep_alive +\ntopology since Stage 0-2, identity Stage 3, mode_conflict Stage 4, layout Stage 5) — the console\nreads this to know which controls are live vs. \"coming soon\" (per-backend nuance, e.g. layout\nposition apply being KWin-only, is reported per display in `/display/state`)."
          },
          "presets": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PresetInfo"
            },
            "description": "Every named preset and what it expands to (for the picker's preview)."
          },
          "settings": {
            "$ref": "#/components/schemas/DisplayPolicy",
            "description": "The stored policy (preset + custom fields), or the built-in default when unconfigured."
          }
        }
      },
      "DisplayStateResponse": {
        "type": "object",
        "description": "The host's managed virtual displays right now.",
        "required": [
          "displays"
        ],
        "properties": {
          "displays": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ApiDisplayInfo"
            }
          }
        }
      },
      "EffectivePolicy": {
        "type": "object",
        "description": "The six resolved fields after preset expansion — what the lifecycle/registry and the Stage-0 call\nsites read, and what the mgmt API echoes as the \"currently in force\" policy. Pure output of\n[`DisplayPolicy::effective`].",
        "required": [
          "keep_alive",
          "topology",
          "mode_conflict",
          "identity",
          "layout",
          "max_displays"
        ],
        "properties": {
          "identity": {
            "$ref": "#/components/schemas/Identity"
          },
          "keep_alive": {
            "$ref": "#/components/schemas/KeepAlive"
          },
          "layout": {
            "$ref": "#/components/schemas/Layout"
          },
          "max_displays": {
            "type": "integer",
            "format": "int32",
            "minimum": 0
          },
          "mode_conflict": {
            "$ref": "#/components/schemas/ModeConflict"
          },
          "topology": {
            "$ref": "#/components/schemas/Topology"
          }
        }
      },
      "EndGameRequest": {
        "type": "object",
        "description": "Request body for `endGame`.",
        "properties": {
          "app_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "Store-qualified library id (`steam:570`) to end; omit to end every waiting game."
          }
        }
      },
      "EndGameResult": {
        "type": "object",
        "description": "Result of an `endGame`.",
        "required": [
          "ended"
        ],
        "properties": {
          "ended": {
            "type": "integer",
            "description": "How many waiting games were ended.",
            "minimum": 0
          }
        }
      },
      "EventKind": {
        "oneOf": [
          {
            "type": "object",
            "required": [
              "client",
              "kind"
            ],
            "properties": {
              "client": {
                "$ref": "#/components/schemas/ClientRef"
              },
              "kind": {
                "type": "string",
                "enum": [
                  "client.connected"
                ]
              }
            }
          },
          {
            "type": "object",
            "required": [
              "client",
              "reason",
              "kind"
            ],
            "properties": {
              "client": {
                "$ref": "#/components/schemas/ClientRef"
              },
              "kind": {
                "type": "string",
                "enum": [
                  "client.disconnected"
                ]
              },
              "reason": {
                "$ref": "#/components/schemas/DisconnectReason"
              }
            }
          },
          {
            "type": "object",
            "required": [
              "session",
              "kind"
            ],
            "properties": {
              "kind": {
                "type": "string",
                "enum": [
                  "session.started"
                ]
              },
              "session": {
                "$ref": "#/components/schemas/SessionRef"
              }
            }
          },
          {
            "type": "object",
            "required": [
              "session",
              "kind"
            ],
            "properties": {
              "kind": {
                "type": "string",
                "enum": [
                  "session.ended"
                ]
              },
              "session": {
                "$ref": "#/components/schemas/SessionRef"
              }
            }
          },
          {
            "type": "object",
            "required": [
              "stream",
              "kind"
            ],
            "properties": {
              "kind": {
                "type": "string",
                "enum": [
                  "stream.started"
                ]
              },
              "stream": {
                "$ref": "#/components/schemas/StreamRef"
              }
            }
          },
          {
            "type": "object",
            "required": [
              "stream",
              "kind"
            ],
            "properties": {
              "kind": {
                "type": "string",
                "enum": [
                  "stream.stopped"
                ]
              },
              "stream": {
                "$ref": "#/components/schemas/StreamRef"
              }
            }
          },
          {
            "type": "object",
            "description": "A launched game was confirmed running — fires once per launch, after the host has actually\nseen the game's process (not merely spawned its launcher).",
            "required": [
              "game",
              "kind"
            ],
            "properties": {
              "game": {
                "$ref": "#/components/schemas/GameRefPayload"
              },
              "kind": {
                "type": "string",
                "enum": [
                  "game.running"
                ]
              }
            }
          },
          {
            "type": "object",
            "description": "A launched game is gone. `reason` distinguishes the player quitting from the host ending it\nper the lifetime policy.",
            "required": [
              "game",
              "reason",
              "kind"
            ],
            "properties": {
              "game": {
                "$ref": "#/components/schemas/GameRefPayload"
              },
              "kind": {
                "type": "string",
                "enum": [
                  "game.exited"
                ]
              },
              "reason": {
                "$ref": "#/components/schemas/GameEndReason"
              }
            }
          },
          {
            "type": "object",
            "required": [
              "device",
              "kind"
            ],
            "properties": {
              "device": {
                "$ref": "#/components/schemas/DeviceRef"
              },
              "kind": {
                "type": "string",
                "enum": [
                  "pairing.pending"
                ]
              }
            }
          },
          {
            "type": "object",
            "required": [
              "device",
              "kind"
            ],
            "properties": {
              "device": {
                "$ref": "#/components/schemas/DeviceRef"
              },
              "kind": {
                "type": "string",
                "enum": [
                  "pairing.completed"
                ]
              }
            }
          },
          {
            "type": "object",
            "required": [
              "device",
              "kind"
            ],
            "properties": {
              "device": {
                "$ref": "#/components/schemas/DeviceRef"
              },
              "kind": {
                "type": "string",
                "enum": [
                  "pairing.denied"
                ]
              }
            }
          },
          {
            "type": "object",
            "required": [
              "backend",
              "mode",
              "kind"
            ],
            "properties": {
              "backend": {
                "type": "string",
                "description": "The virtual-display backend that minted it (`VirtualDisplay::name`)."
              },
              "kind": {
                "type": "string",
                "enum": [
                  "display.created"
                ]
              },
              "mode": {
                "type": "string",
                "description": "`WxH@Hz`."
              }
            }
          },
          {
            "type": "object",
            "required": [
              "count",
              "kind"
            ],
            "properties": {
              "count": {
                "type": "integer",
                "format": "int32",
                "description": "How many kept displays this release retired.",
                "minimum": 0
              },
              "kind": {
                "type": "string",
                "enum": [
                  "display.released"
                ]
              }
            }
          },
          {
            "type": "object",
            "required": [
              "source",
              "kind"
            ],
            "properties": {
              "kind": {
                "type": "string",
                "enum": [
                  "library.changed"
                ]
              },
              "source": {
                "type": "string",
                "description": "What mutated the library: `\"manual\"` today; a provider id once the provider\nAPI (RFC §8) lands."
              }
            }
          },
          {
            "type": "object",
            "required": [
              "id",
              "kind"
            ],
            "properties": {
              "id": {
                "type": "string",
                "description": "The plugin whose registration changed (registered, restarted, deregistered, or\nlease-expired). A consumer re-reads `GET /api/v1/plugins` for the new set."
              },
              "kind": {
                "type": "string",
                "enum": [
                  "plugins.changed"
                ]
              }
            }
          },
          {
            "type": "object",
            "description": "The set of installed plugins, or what the store knows about them, changed — an install or\nuninstall finished, or a catalog refresh brought in new rows. A consumer re-reads\n`GET /api/v1/store/catalog` / `…/installed`. Deliberately payload-free: the store's answer\nis a join over several sources of truth, so \"go look again\" is the only honest signal.",
            "required": [
              "kind"
            ],
            "properties": {
              "kind": {
                "type": "string",
                "enum": [
                  "store.changed"
                ]
              }
            }
          },
          {
            "type": "object",
            "required": [
              "version",
              "gamestream",
              "kind"
            ],
            "properties": {
              "gamestream": {
                "type": "boolean",
                "description": "Whether the GameStream/Moonlight compat plane is enabled."
              },
              "kind": {
                "type": "string",
                "enum": [
                  "host.started"
                ]
              },
              "version": {
                "type": "string"
              }
            }
          },
          {
            "type": "object",
            "required": [
              "kind"
            ],
            "properties": {
              "kind": {
                "type": "string",
                "enum": [
                  "host.stopping"
                ]
              }
            }
          }
        ],
        "description": "The event catalog (RFC §4). Serialized internally tagged as `\"kind\": \"<domain>.<verb>\"`,\nflattened into [`HostEvent`]. **Additive-only** within [`SCHEMA_VERSION`]."
      },
      "GameEndReason": {
        "type": "string",
        "description": "Why a launched game is no longer running.",
        "enum": [
          "exited",
          "terminated"
        ]
      },
      "GameEntry": {
        "allOf": [
          {
            "$ref": "#/components/schemas/GameMeta",
            "description": "Descriptive metadata, flattened — see [`GameMeta`]."
          },
          {
            "type": "object",
            "required": [
              "id",
              "store",
              "title",
              "art"
            ],
            "properties": {
              "art": {
                "$ref": "#/components/schemas/Artwork"
              },
              "id": {
                "type": "string",
                "description": "Stable, store-qualified id: `steam:<appid>` or `custom:<id>`.",
                "example": "steam:570"
              },
              "launch": {
                "oneOf": [
                  {
                    "type": "null"
                  },
                  {
                    "$ref": "#/components/schemas/LaunchSpec",
                    "description": "How the host would launch it, when known."
                  }
                ]
              },
              "provider": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The external provider owning this entry (custom-store entries synced by a provider\nplugin, RFC §8) — `None` for installed-store titles and manual custom entries. The\nconsole uses it for attribution; `GET /library?provider=` filters on it."
              },
              "store": {
                "type": "string",
                "description": "Which store surfaced it: `\"steam\"` or `\"custom\"`.",
                "example": "steam"
              },
              "title": {
                "type": "string"
              }
            }
          }
        ],
        "description": "One title in the unified library, regardless of which store it came from."
      },
      "GameMeta": {
        "type": "object",
        "description": "Descriptive metadata for a title — everything a richer library UI (details pane, platform\nfilter, couch-pick badges) renders beyond the poster. Every field is optional and defaults to\nabsent, so pre-metadata catalogs, providers, and clients keep working unchanged. The struct is\n`#[serde(flatten)]`-ed into [`GameEntry`] / the custom-store shapes: one definition, a flat\nwire shape everywhere.\n\nValues are free-form display strings, not enums — emulation sources (RomM, EmuDeck, Playnite)\neach have their own vocabulary and the host has no business normalizing it.",
        "properties": {
          "description": {
            "type": [
              "string",
              "null"
            ],
            "description": "Short blurb for a details pane."
          },
          "developer": {
            "type": [
              "string",
              "null"
            ]
          },
          "genres": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Genre taxonomy from the metadata source (`\"RPG\"`, `\"Platformer\"`, …)."
          },
          "platform": {
            "type": [
              "string",
              "null"
            ],
            "description": "The system the title runs on — `\"PS2\"`, `\"Xbox 360\"`, `\"SNES\"`, … Installed-store\nscanners stamp `\"PC\"`; `GET /library?platform=` filters on it (case-insensitive).",
            "example": "PS2"
          },
          "players": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int32",
            "description": "Maximum simultaneous (local) players.",
            "minimum": 0
          },
          "publisher": {
            "type": [
              "string",
              "null"
            ]
          },
          "region": {
            "type": [
              "string",
              "null"
            ],
            "description": "Release region — emulation-relevant (`\"NTSC-U\"`, `\"PAL\"`, `\"NTSC-J\"`)."
          },
          "release_year": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int32",
            "description": "Year of first release — the granularity metadata sources reliably agree on.",
            "example": 2001,
            "minimum": 0
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Free-form organizational labels (`\"co-op\"`, `\"kids\"`, `\"finished\"`, …)."
          }
        }
      },
      "GameOnSessionEnd": {
        "type": "string",
        "description": "What to do with the launched game when its session ends.",
        "enum": [
          "keep",
          "on_quit",
          "always"
        ]
      },
      "GameRefPayload": {
        "type": "object",
        "description": "A launched game, as the `game.*` events see it.",
        "required": [
          "title",
          "client",
          "plane"
        ],
        "properties": {
          "app": {
            "type": [
              "string",
              "null"
            ],
            "description": "Store-qualified library id (`steam:570`). Absent for an operator-typed GameStream\n`apps.json` command, which has no library entry behind it."
          },
          "client": {
            "type": "string",
            "description": "Client-supplied device name of the session that launched it; may be empty."
          },
          "plane": {
            "$ref": "#/components/schemas/Plane"
          },
          "store": {
            "type": [
              "string",
              "null"
            ],
            "description": "Which store surfaced it (`steam`, `heroic`, `custom`, …), when known."
          },
          "title": {
            "type": "string",
            "description": "Display title."
          }
        }
      },
      "GameSession": {
        "type": "string",
        "description": "How a session that **launches a game** (a library id on the Hello / apps.json / Decky pin) is\nserved (`design/gamemode-and-dedicated-sessions.md` §5.2). Orthogonal to the preset/lifecycle axes\n— a top-level [`DisplayPolicy`] field, NOT part of [`EffectivePolicy`], so a preset never clobbers\nit. Linux-only in effect (a launching Windows session opens into the one desktop).",
        "enum": [
          "auto",
          "dedicated"
        ]
      },
      "GpuState": {
        "type": "object",
        "description": "Full GPU-selection state for the console: inventory, the persisted preference, what the next\nsession will use, and what is in use right now.",
        "required": [
          "gpus",
          "mode",
          "preferred_available"
        ],
        "properties": {
          "active": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/ApiActiveGpu",
                "description": "The GPU live sessions use right now (absent while nothing is streaming)."
              }
            ]
          },
          "encoder_pin": {
            "type": [
              "string",
              "null"
            ],
            "description": "`PUNKTFUNK_ENCODER` (the host.env encoder pin), when set to something other than `auto`\n(e.g. `qsv`, `nvenc`, `amf`, `software`). A pin whose vendor contradicts the selected\nGPU is overridden at session open — the adapter wins — so the console can warn that the\npin is stale rather than letting the selection look broken."
          },
          "env_override": {
            "type": [
              "string",
              "null"
            ],
            "description": "`PUNKTFUNK_RENDER_ADAPTER` (the host.env pin), when set — it applies while `mode` is\n`auto`; a manual preference overrides it."
          },
          "gpus": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ApiGpu"
            },
            "description": "The host's hardware GPUs."
          },
          "mode": {
            "type": "string",
            "description": "`auto` or `manual`."
          },
          "preferred_available": {
            "type": "boolean",
            "description": "Whether the preferred GPU is currently present."
          },
          "preferred_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "The manually preferred GPU's stable id, when one is stored (kept while `mode` is `auto` so\na console can offer returning to it). May reference a GPU that is currently absent."
          },
          "preferred_name": {
            "type": [
              "string",
              "null"
            ],
            "description": "The stored name of the preferred GPU (a usable label even when it is absent)."
          },
          "selected": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/ApiSelectedGpu",
                "description": "The GPU the next session will use."
              }
            ]
          }
        }
      },
      "Health": {
        "type": "object",
        "description": "Liveness + version probe.",
        "required": [
          "status",
          "version",
          "abi_version"
        ],
        "properties": {
          "abi_version": {
            "type": "integer",
            "format": "int32",
            "description": "`punktfunk-core` C ABI version.",
            "minimum": 0
          },
          "status": {
            "type": "string",
            "description": "Always `\"ok\"` when the host responds.",
            "example": "ok"
          },
          "version": {
            "type": "string",
            "description": "`punktfunk-host` crate version."
          }
        }
      },
      "HookEntry": {
        "type": "object",
        "description": "One hook: fire `run` and/or `webhook` when an event matching `on` (+ `filter`) occurs.",
        "required": [
          "on"
        ],
        "properties": {
          "debounce_ms": {
            "type": "integer",
            "format": "int64",
            "description": "Minimum interval between firings of this hook, in milliseconds. 0 = fire every time.",
            "minimum": 0
          },
          "filter": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/HookFilter",
                "description": "Exact-match constraints on the event's fields; every present field must match."
              }
            ]
          },
          "hmac_secret_file": {
            "type": [
              "string",
              "null"
            ],
            "description": "File holding the webhook HMAC secret (`X-Punktfunk-Signature: sha256=<hex>`). The file\nshould be operator-owned and private; a world-readable secret is warned about."
          },
          "on": {
            "type": "string",
            "description": "Which events fire this hook: an exact kind (`stream.started`) or a `domain.*` prefix\n(`pairing.*`) — the same vocabulary as the SSE `?kinds=` filter."
          },
          "run": {
            "type": [
              "string",
              "null"
            ],
            "description": "Shell command to execute (detached, event JSON on stdin + `PF_EVENT_*` env)."
          },
          "timeout_s": {
            "type": "integer",
            "format": "int32",
            "description": "Exec timeout in seconds (1–600, default 30); the process group is killed on expiry.",
            "minimum": 0
          },
          "webhook": {
            "type": [
              "string",
              "null"
            ],
            "description": "URL to POST the event JSON to."
          }
        }
      },
      "HookFilter": {
        "type": "object",
        "description": "Exact-match filters against an event's identity fields (RFC open-question 3: exact match\nonly — anything richer is what the SDK is for). Absent fields don't constrain; a filter\nfield set on an event kind that doesn't carry it (e.g. `client` on `host.started`) never\nmatches.",
        "properties": {
          "app": {
            "type": [
              "string",
              "null"
            ],
            "description": "Launched app id/title (`stream.*` events)."
          },
          "client": {
            "type": [
              "string",
              "null"
            ],
            "description": "Client/device name (for `session.*`: the short client label the Dashboard shows)."
          },
          "fingerprint": {
            "type": [
              "string",
              "null"
            ],
            "description": "Certificate fingerprint (hex, case-insensitive)."
          },
          "plane": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/Plane",
                "description": "Protocol plane (`native` / `gamestream`)."
              }
            ]
          }
        }
      },
      "HooksConfig": {
        "type": "object",
        "description": "The operator's hook configuration — the `hooks.json` document and the `/api/v1/hooks` body.",
        "properties": {
          "hooks": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/HookEntry"
            }
          }
        }
      },
      "HostEvent": {
        "allOf": [
          {
            "$ref": "#/components/schemas/EventKind",
            "description": "The event kind + payload, flattened: `\"kind\": \"stream.started\", …payload…`."
          },
          {
            "type": "object",
            "required": [
              "seq",
              "ts_ms",
              "schema"
            ],
            "properties": {
              "schema": {
                "type": "integer",
                "format": "int32",
                "description": "Wire-shape version ([`SCHEMA_VERSION`]).",
                "minimum": 0
              },
              "seq": {
                "type": "integer",
                "format": "int64",
                "description": "Monotonic sequence number (1-based) — a consumer resumes with `since = last seen`.",
                "minimum": 0
              },
              "ts_ms": {
                "type": "integer",
                "format": "int64",
                "description": "Unix timestamp in milliseconds (the [`crate::log_capture::LogEntry`] convention).",
                "minimum": 0
              }
            }
          }
        ],
        "description": "One host lifecycle event, as it will appear on the wire (`data:` of one SSE frame)."
      },
      "HostFacts": {
        "type": "object",
        "description": "Facts about this host, so the console can grey out rows it can't install.",
        "required": [
          "version",
          "platform"
        ],
        "properties": {
          "platform": {
            "type": "string",
            "description": "`linux` / `windows` / `macos`."
          },
          "version": {
            "type": "string"
          }
        }
      },
      "HostInfo": {
        "type": "object",
        "description": "Host identity and advertised capabilities (static for the life of the process).",
        "required": [
          "hostname",
          "uniqueid",
          "local_ip",
          "version",
          "abi_version",
          "app_version",
          "gfe_version",
          "codecs",
          "gamestream",
          "ports"
        ],
        "properties": {
          "abi_version": {
            "type": "integer",
            "format": "int32",
            "description": "`punktfunk-core` C ABI version.",
            "minimum": 0
          },
          "app_version": {
            "type": "string",
            "description": "GameStream host version advertised to Moonlight clients."
          },
          "codecs": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ApiCodec"
            },
            "description": "Codecs the host can encode (NVENC)."
          },
          "gamestream": {
            "type": "boolean",
            "description": "Whether the GameStream/Moonlight-compat planes are running (`--gamestream`). `false` on the\nsecure default (native punktfunk/1 only) — a console can hide Moonlight-only UI (e.g. the\nMoonlight PIN pairing card, which could never receive a PIN when this is `false`)."
          },
          "gfe_version": {
            "type": "string",
            "description": "GFE version advertised to Moonlight clients."
          },
          "hostname": {
            "type": "string"
          },
          "local_ip": {
            "type": "string",
            "description": "Best-effort primary LAN IP."
          },
          "ports": {
            "$ref": "#/components/schemas/PortMap"
          },
          "uniqueid": {
            "type": "string",
            "description": "Stable per-host id (persisted across restarts), matched on pairing."
          },
          "version": {
            "type": "string",
            "description": "`punktfunk-host` crate version."
          }
        }
      },
      "Identity": {
        "type": "string",
        "description": "Stable display identity, so desktop environments persist per-display config (KDE scaling). Stored\nat Stage 0; carriers wired from the identity stage.",
        "enum": [
          "shared",
          "per-client",
          "per-client-mode"
        ]
      },
      "InstallRequest": {
        "type": "object",
        "description": "`POST /store/install` — either a catalogued entry, or a raw spec the operator owns.",
        "properties": {
          "accept_unverified": {
            "type": "boolean",
            "description": "Required with [`Self::spec`]: the operator's explicit acknowledgement that this installs\nunreviewed code with operator privileges. The console collects it behind a typed\nconfirmation; the API refuses without it so no other caller can skip the decision."
          },
          "id": {
            "type": [
              "string",
              "null"
            ],
            "description": "Catalog entry id (with [`Self::source`])."
          },
          "source": {
            "type": [
              "string",
              "null"
            ],
            "description": "Catalog source name (with [`Self::id`])."
          },
          "spec": {
            "type": [
              "string",
              "null"
            ],
            "description": "A raw package spec (`@scope/name`, `@scope/name@1.2.3`, an https tarball or git+https URL).\nNothing reviewed it and nothing pins it."
          }
        }
      },
      "InstalledView": {
        "type": "object",
        "description": "An installed plugin package, joined with its provenance and whether it's actually running.",
        "required": [
          "pkg",
          "tier",
          "running"
        ],
        "properties": {
          "blocked": {
            "type": [
              "string",
              "null"
            ],
            "description": "A revocation covering the *installed* version. Reported, never auto-removed: silently\ndeleting running code is its own hazard, so the operator decides."
          },
          "entry_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "The catalog entry this maps to, when it is on a shelf we know."
          },
          "installed_at": {
            "type": [
              "string",
              "null"
            ]
          },
          "pkg": {
            "type": "string"
          },
          "plugin_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "The plugin id it registers under — the key into `GET /plugins`."
          },
          "running": {
            "type": "boolean",
            "description": "Is it registered in the live lease registry right now?"
          },
          "source": {
            "type": [
              "string",
              "null"
            ]
          },
          "tier": {
            "type": "string",
            "description": "`verified` / `external` / `unverified` / `cli` — remembered from install time, so an\nunverified plugin stays visibly unverified long after the dialog is forgotten."
          },
          "title": {
            "type": [
              "string",
              "null"
            ]
          },
          "update_available": {
            "type": [
              "string",
              "null"
            ],
            "description": "The catalog's version, when it's newer than what's installed."
          },
          "version": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "Job": {
        "type": "object",
        "description": "A job as the console sees it. Field names are snake_case like the rest of the management API\n(the *file* formats — index, sources, manifest — follow npm's camelCase instead).",
        "required": [
          "id",
          "kind",
          "target",
          "state",
          "phase",
          "log",
          "started_at"
        ],
        "properties": {
          "error": {
            "type": [
              "string",
              "null"
            ]
          },
          "finished_at": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64",
            "minimum": 0
          },
          "id": {
            "type": "string"
          },
          "kind": {
            "type": "string",
            "description": "`install` or `uninstall`."
          },
          "log": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Tail of the runner's combined stdout/stderr."
          },
          "phase": {
            "type": "string",
            "description": "Coarse step name, for a progress line the operator can read."
          },
          "started_at": {
            "type": "integer",
            "format": "int64",
            "minimum": 0
          },
          "state": {
            "$ref": "#/components/schemas/State"
          },
          "target": {
            "type": "string",
            "description": "What the operator asked for — a package name, or the raw spec they typed."
          }
        }
      },
      "JobRef": {
        "type": "object",
        "description": "202 body: where to watch the work.",
        "required": [
          "job"
        ],
        "properties": {
          "job": {
            "type": "string"
          }
        }
      },
      "KeepAlive": {
        "oneOf": [
          {
            "type": "object",
            "description": "Tear the display down at session end (today's default on every backend but Windows, which\nlingers 10 s).",
            "required": [
              "mode"
            ],
            "properties": {
              "mode": {
                "type": "string",
                "enum": [
                  "off"
                ]
              }
            }
          },
          {
            "type": "object",
            "description": "Keep the display for `seconds` after the last session leaves, then tear it down; a reconnect\ninside the window reuses it.",
            "required": [
              "seconds",
              "mode"
            ],
            "properties": {
              "mode": {
                "type": "string",
                "enum": [
                  "duration"
                ]
              },
              "seconds": {
                "type": "integer",
                "format": "int32",
                "description": "Linger window in seconds.",
                "minimum": 0
              }
            }
          },
          {
            "type": "object",
            "description": "Keep the display until host shutdown or an explicit release (the `Pinned` lifecycle state).\n**Not honored until the display-lifecycle stage** — rejected by the mgmt PUT at Stage 0.",
            "required": [
              "mode"
            ],
            "properties": {
              "mode": {
                "type": "string",
                "enum": [
                  "forever"
                ]
              }
            }
          }
        ],
        "description": "How long a virtual display (and, on gamescope's bare spawn, the nested session + its game)\nsurvives after the last client session detaches. Serialized as an object tagged on `mode`\n(`{\"mode\":\"off\"}` / `{\"mode\":\"duration\",\"seconds\":300}` / `{\"mode\":\"forever\"}`) so the web form\nand the OpenAPI schema stay simple."
      },
      "LaunchSpec": {
        "type": "object",
        "description": "How the host would launch a title (consumed by the session launcher in a later step). Kept\nopen-ended so new stores slot in: `steam_appid` → `steam steam://rungameid/<value>`;\n`command` → run `<value>` nested in a gamescope session.",
        "required": [
          "kind",
          "value"
        ],
        "properties": {
          "kind": {
            "type": "string",
            "description": "`\"steam_appid\"` or `\"command\"`.",
            "example": "steam_appid"
          },
          "value": {
            "type": "string",
            "description": "The appid (for `steam_appid`) or the shell command (for `command`)."
          }
        }
      },
      "Layout": {
        "type": "object",
        "description": "Group layout: the arrangement mode plus, for [`LayoutMode::Manual`], per-slot offsets keyed by\nidentity-slot id (string keys for stable JSON).",
        "properties": {
          "mode": {
            "$ref": "#/components/schemas/LayoutMode"
          },
          "positions": {
            "type": "object",
            "additionalProperties": {
              "$ref": "#/components/schemas/Position"
            },
            "propertyNames": {
              "type": "string"
            }
          }
        }
      },
      "LayoutMode": {
        "type": "string",
        "description": "How group members are arranged in the desktop coordinate space. Stored at Stage 0; applied from\nthe multi-monitor stage.",
        "enum": [
          "auto-row",
          "manual"
        ]
      },
      "LocalSummary": {
        "type": "object",
        "description": "Non-sensitive host status for the local tray icon: counts and booleans only — no PIN values,\nno fingerprints, no device names. Served unauthenticated to LOOPBACK peers only (see\n`require_auth`): the bearer-token file is SYSTEM/Administrators-DACL'd on Windows, so the\nper-user tray process cannot authenticate — this narrow read-only route is its status source.",
        "required": [
          "version",
          "video_streaming",
          "audio_streaming",
          "paired_clients",
          "native_paired_clients",
          "pin_pending",
          "pending_approvals",
          "kept_displays"
        ],
        "properties": {
          "audio_streaming": {
            "type": "boolean",
            "description": "True while audio is streaming on either plane (same rule as `video_streaming`)."
          },
          "conflicts": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Other Moonlight-compatible hosts (Sunshine/Apollo/…) detected on this machine at startup —\nrunning one alongside Punktfunk is unsupported. Compact labels (e.g. `Sunshine (running)`);\nthe tray/console surface them so the clash is visible before pairing silently fails."
          },
          "games": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Launched games the host is tracking, as compact labels (`Hades`, `Hades (closing in 4:12)`).\n\nThe countdown form is the one that matters: it means the game's client is gone and the host\nwill end the game when the window closes — something a user at the machine should be able to\nsee (and stop) without opening the console. Empty when nothing was launched."
          },
          "kept_displays": {
            "type": "integer",
            "format": "int32",
            "description": "Virtual displays being KEPT with no live session — lingering (keep-alive window) or pinned\n(`keep_alive: forever`). Non-zero means a display (and, exclusive, your physical monitors) is\nheld; the tray surfaces it + a one-click release. Active (in-use) displays are not counted.",
            "minimum": 0
          },
          "native_paired_clients": {
            "type": "integer",
            "format": "int32",
            "description": "Number of paired native (punktfunk/1) devices.",
            "minimum": 0
          },
          "paired_clients": {
            "type": "integer",
            "format": "int32",
            "description": "Number of pinned (paired) GameStream client certificates.",
            "minimum": 0
          },
          "pending_approvals": {
            "type": "integer",
            "format": "int32",
            "description": "Native pairing knocks awaiting the operator's approval (count only).",
            "minimum": 0
          },
          "pin_pending": {
            "type": "boolean",
            "description": "True while a GameStream pairing handshake is parked waiting for the user's PIN."
          },
          "session": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/SessionInfo",
                "description": "The active session: GameStream's launch (Moonlight `/launch`) when present, else the first\nlive native session. `null` when nothing is streaming."
              }
            ]
          },
          "version": {
            "type": "string",
            "description": "Host version (mirrors `/health`)."
          },
          "video_streaming": {
            "type": "boolean",
            "description": "True while video is streaming on EITHER plane: the GameStream media pipeline, or a live\nnative (punktfunk/1) session — the default plane, invisible in the GameStream flag alone."
          }
        }
      },
      "LogEntry": {
        "type": "object",
        "description": "One captured log event.",
        "required": [
          "seq",
          "ts_ms",
          "level",
          "target",
          "msg"
        ],
        "properties": {
          "level": {
            "type": "string",
            "description": "`ERROR` | `WARN` | `INFO` | `DEBUG` | `TRACE`."
          },
          "msg": {
            "type": "string",
            "description": "The formatted message, structured fields appended as `key=value`."
          },
          "seq": {
            "type": "integer",
            "format": "int64",
            "description": "Monotonic sequence number (1-based) — pass the last one back as the `after` cursor.",
            "minimum": 0
          },
          "target": {
            "type": "string",
            "description": "The emitting module path (tracing target)."
          },
          "ts_ms": {
            "type": "integer",
            "format": "int64",
            "description": "Unix timestamp in milliseconds.",
            "minimum": 0
          }
        }
      },
      "LogPage": {
        "type": "object",
        "description": "One poll's worth of log entries.",
        "required": [
          "entries",
          "next",
          "dropped"
        ],
        "properties": {
          "dropped": {
            "type": "boolean",
            "description": "True when entries between `after` and the first returned one were already evicted."
          },
          "entries": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/LogEntry"
            }
          },
          "next": {
            "type": "integer",
            "format": "int64",
            "description": "Cursor for the next poll (the last returned seq, or the request's `after` when empty).",
            "minimum": 0
          }
        }
      },
      "ModeConflict": {
        "type": "string",
        "description": "Admission when a *different* client connects while a display/session is already live and asks for\na different mode. Stored at Stage 0; enforced from the mode-conflict admission stage.",
        "enum": [
          "separate",
          "steal",
          "join",
          "reject"
        ]
      },
      "NativeClient": {
        "type": "object",
        "description": "A paired native (punktfunk/1) client.",
        "required": [
          "name",
          "fingerprint"
        ],
        "properties": {
          "fingerprint": {
            "type": "string",
            "description": "Hex SHA-256 of the client certificate — its stable id here."
          },
          "name": {
            "type": "string",
            "description": "The name the client supplied when pairing.",
            "example": "Living Room iPad"
          }
        }
      },
      "NativePairStatus": {
        "type": "object",
        "description": "Native (punktfunk/1) pairing status. Unlike GameStream, the **host** mints the PIN (the SPAKE2\nceremony needs it client-side first), so the console **displays** `pin` for the user to enter on\ntheir device — armed on demand for a short window.",
        "required": [
          "enabled",
          "armed",
          "paired_clients"
        ],
        "properties": {
          "armed": {
            "type": "boolean",
            "description": "True while a pairing window is open."
          },
          "enabled": {
            "type": "boolean",
            "description": "Whether the native host is running (the unified host started with `--native`)."
          },
          "expires_in_secs": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64",
            "description": "Seconds left in the window (null = disarmed, or armed with no expiry via the CLI flag).",
            "minimum": 0
          },
          "paired_clients": {
            "type": "integer",
            "format": "int32",
            "description": "Number of paired native clients.",
            "minimum": 0
          },
          "pin": {
            "type": [
              "string",
              "null"
            ],
            "description": "The PIN to display while armed (null when disarmed).",
            "example": "1234"
          }
        }
      },
      "PairedClient": {
        "type": "object",
        "description": "A paired (certificate-pinned) Moonlight client.",
        "required": [
          "fingerprint"
        ],
        "properties": {
          "fingerprint": {
            "type": "string",
            "description": "Lowercase hex SHA-256 of the client certificate DER — the client's stable id here.",
            "example": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
          },
          "not_after_unix": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64",
            "description": "Certificate validity end (unix seconds)."
          },
          "not_before_unix": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64",
            "description": "Certificate validity start (unix seconds)."
          },
          "subject": {
            "type": [
              "string",
              "null"
            ],
            "description": "Certificate subject (e.g. `CN=NVIDIA GameStream Client`), if the DER parses."
          }
        }
      },
      "PairingStatus": {
        "type": "object",
        "description": "Pairing-flow status.",
        "required": [
          "pin_pending"
        ],
        "properties": {
          "pin_pending": {
            "type": "boolean",
            "description": "True while a pairing handshake is parked waiting for the user's PIN."
          }
        }
      },
      "PendingDevice": {
        "type": "object",
        "description": "An unpaired device that tried to connect while the host requires pairing — awaiting\n**delegated approval** (approve it here instead of fetching the host PIN out of band).",
        "required": [
          "id",
          "name",
          "fingerprint",
          "age_secs"
        ],
        "properties": {
          "age_secs": {
            "type": "integer",
            "format": "int64",
            "description": "Seconds since the device last knocked.",
            "minimum": 0
          },
          "fingerprint": {
            "type": "string",
            "description": "Hex SHA-256 of the device's certificate — what approval pins."
          },
          "id": {
            "type": "integer",
            "format": "int32",
            "description": "Id to address approve/deny (per-process; entries expire after ~10 minutes).",
            "minimum": 0
          },
          "name": {
            "type": "string",
            "description": "Best-effort device label (the client's own name, else fingerprint-derived).",
            "example": "Enrico's MacBook"
          }
        }
      },
      "Plane": {
        "type": "string",
        "description": "Which protocol plane an event originated from. Hooks and scripts filter on it — a hook\nthat fires for native clients but not Moonlight clients is a bug, not a v2 feature.",
        "enum": [
          "native",
          "gamestream"
        ]
      },
      "PluginRegistration": {
        "type": "object",
        "description": "Register/renew body for `PUT /plugins/{id}`.",
        "required": [
          "title"
        ],
        "properties": {
          "title": {
            "type": "string",
            "description": "Human-readable title for the console nav entry (1–64 chars; control chars stripped)."
          },
          "ui": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/PluginUi",
                "description": "Present iff the plugin serves a UI surface. A registration with no `ui` is a liveness/phone-book\nentry only (e.g. a future runner-management listing) and grows no nav entry."
              }
            ]
          },
          "version": {
            "type": [
              "string",
              "null"
            ],
            "description": "Optional plugin version, purely informational (≤32 chars)."
          }
        }
      },
      "PluginSummary": {
        "type": "object",
        "description": "One entry in `GET /plugins`. **Never carries the secret** — the browser learns a plugin exists\nand has a UI, nothing that lets it reach the plugin directly (it goes through the console proxy).",
        "required": [
          "id",
          "title"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "ui": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/PluginUiPublic"
              }
            ]
          },
          "version": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "PluginUi": {
        "type": "object",
        "description": "A plugin's UI surface as it registers it. Carries the secret — this shape is only ever a request\nbody, never a response ([`PluginUiPublic`] is the secret-free view).",
        "required": [
          "port",
          "secret"
        ],
        "properties": {
          "icon": {
            "type": [
              "string",
              "null"
            ],
            "description": "Optional lucide icon name for the console nav entry (`^[a-z0-9-]{1,48}$`)."
          },
          "port": {
            "type": "integer",
            "format": "int32",
            "description": "The **loopback** port the plugin serves its UI on. The host and console only ever dial\n`127.0.0.1:<port>`; a registration can never carry a hostname.",
            "minimum": 0
          },
          "secret": {
            "type": "string",
            "description": "Per-boot shared secret the console proxy must present (as `Authorization: Bearer`) on every\nrequest to the plugin's UI server. Rotated whenever the plugin restarts."
          }
        }
      },
      "PluginUiPublic": {
        "type": "object",
        "description": "The secret-free view of a plugin's UI surface — what [`list_plugins`] returns to the browser.",
        "required": [
          "port"
        ],
        "properties": {
          "icon": {
            "type": [
              "string",
              "null"
            ]
          },
          "port": {
            "type": "integer",
            "format": "int32",
            "minimum": 0
          }
        }
      },
      "PortMap": {
        "type": "object",
        "description": "Every port a client integration may need (Moonlight derives the stream ports from the\nHTTP base; a control pane should not have to).",
        "required": [
          "mgmt",
          "http",
          "https",
          "rtsp",
          "video",
          "control",
          "audio"
        ],
        "properties": {
          "audio": {
            "type": "integer",
            "format": "int32",
            "minimum": 0
          },
          "control": {
            "type": "integer",
            "format": "int32",
            "minimum": 0
          },
          "http": {
            "type": "integer",
            "format": "int32",
            "description": "nvhttp plain HTTP (serverinfo, pairing).",
            "minimum": 0
          },
          "https": {
            "type": "integer",
            "format": "int32",
            "description": "nvhttp mutual-TLS HTTPS (post-pairing).",
            "minimum": 0
          },
          "mgmt": {
            "type": "integer",
            "format": "int32",
            "description": "This management API.",
            "minimum": 0
          },
          "rtsp": {
            "type": "integer",
            "format": "int32",
            "minimum": 0
          },
          "video": {
            "type": "integer",
            "format": "int32",
            "minimum": 0
          }
        }
      },
      "Position": {
        "type": "object",
        "description": "A desktop-space offset for a display (top-left origin).",
        "required": [
          "x",
          "y"
        ],
        "properties": {
          "x": {
            "type": "integer",
            "format": "int32"
          },
          "y": {
            "type": "integer",
            "format": "int32"
          }
        }
      },
      "PrepCmd": {
        "type": "object",
        "description": "One per-app preparation step (RFC §6 — deliberate Sunshine `prep-cmd` parity): `do` runs\n**synchronously before the app launches** (an HDR toggle or a MangoHud env change must land\nfirst), `undo` runs at session end — reverse order across steps, best-effort, on every exit\npath including a crash-unwind (RAII via [`PrepGuard`]).",
        "required": [
          "do"
        ],
        "properties": {
          "do": {
            "type": "string",
            "description": "Command run before launch. Same execution recipe and ownership checks as hook `run`\ncommands (event-less: stdin is empty JSON, env carries the `PF_APP_*` context)."
          },
          "undo": {
            "type": [
              "string",
              "null"
            ],
            "description": "Command run after the session ends. Skipped when its `do` failed (it never took effect)."
          }
        }
      },
      "Preset": {
        "type": "string",
        "description": "A named bundle of the fields below. `Custom` (the default) means the explicit fields rule; any\nother preset ignores the stored fields and expands to its own ([`DisplayPolicy::effective`]).",
        "enum": [
          "custom",
          "default",
          "gaming-rig",
          "shared-desktop",
          "hotdesk",
          "workstation"
        ]
      },
      "PresetInfo": {
        "type": "object",
        "description": "One preset's human-facing description + the fields it expands to, so the console can render a\npreset picker with an accurate \"what this does\" preview without hardcoding the expansion.",
        "required": [
          "id",
          "summary",
          "fields"
        ],
        "properties": {
          "fields": {
            "$ref": "#/components/schemas/EffectivePolicy",
            "description": "The effective policy this preset expands to (the same fields a `custom` policy carries)."
          },
          "id": {
            "type": "string",
            "description": "The preset id (`default` | `gaming-rig` | `shared-desktop` | `hotdesk` | `workstation`)."
          },
          "summary": {
            "type": "string",
            "description": "One-line story shown next to the option."
          }
        }
      },
      "ProviderEntryInput": {
        "allOf": [
          {
            "$ref": "#/components/schemas/GameMeta",
            "description": "Descriptive metadata (platform, description, …), flattened — see [`GameMeta`]."
          },
          {
            "type": "object",
            "required": [
              "external_id",
              "title"
            ],
            "properties": {
              "art": {
                "$ref": "#/components/schemas/Artwork"
              },
              "detect": {
                "$ref": "#/components/schemas/DetectHint",
                "description": "How to recognize this title's process — see [`CustomEntry::detect`]. A provider that knows its\ntitles' install directories (Playnite does) should send them: it is what lets a game launched\nthrough the provider's own client still end its session when the player quits."
              },
              "external_id": {
                "type": "string",
                "description": "The provider's stable id for this title (the reconcile diff key)."
              },
              "launch": {
                "oneOf": [
                  {
                    "type": "null"
                  },
                  {
                    "$ref": "#/components/schemas/LaunchSpec"
                  }
                ]
              },
              "prep": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/PrepCmd"
                },
                "description": "Per-title prep/undo steps — commands run as the host user; operator-privileged config."
              },
              "title": {
                "type": "string"
              }
            }
          }
        ],
        "description": "One title in a provider's declarative reconcile payload (RFC §8): [`CustomInput`] plus the\nprovider's required stable key."
      },
      "ProviderRemoved": {
        "type": "object",
        "description": "The count envelope a provider uninstall returns.",
        "required": [
          "removed"
        ],
        "properties": {
          "removed": {
            "type": "integer",
            "description": "How many entries the provider owned (and were removed).",
            "minimum": 0
          }
        }
      },
      "ReleaseDisplayRequest": {
        "type": "object",
        "description": "Request body for `releaseDisplay`.",
        "properties": {
          "slot": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64",
            "description": "Slot to release (see `state`); omit to release **all** kept displays.",
            "minimum": 0
          }
        }
      },
      "ReleaseDisplayResult": {
        "type": "object",
        "description": "Result of a `/display/release`.",
        "required": [
          "released"
        ],
        "properties": {
          "released": {
            "type": "integer",
            "description": "Number of kept displays torn down.",
            "minimum": 0
          }
        }
      },
      "RuntimeRequest": {
        "type": "object",
        "required": [
          "enabled"
        ],
        "properties": {
          "enabled": {
            "type": "boolean"
          }
        }
      },
      "RuntimeStatus": {
        "type": "object",
        "description": "Live host status (changes as clients launch/end sessions).",
        "required": [
          "video_streaming",
          "audio_streaming",
          "pin_pending",
          "paired_clients",
          "active_sessions",
          "games"
        ],
        "properties": {
          "active_sessions": {
            "type": "integer",
            "format": "int32",
            "description": "Number of live streaming sessions across BOTH planes (GameStream + native punktfunk/1). The\nnative server admits concurrent sessions, so this can exceed 1; `session`/`stream` below\ndescribe a single representative session for the detail card.",
            "minimum": 0
          },
          "audio_streaming": {
            "type": "boolean",
            "description": "True while the audio stream thread is running."
          },
          "games": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ActiveGame"
            },
            "description": "Every launched game the host is tracking: one row per live session that launched a title, plus\nany game whose session has ended and which is waiting out its reconnect window before being\nended (`state: \"grace\"`). Empty when nothing was launched — a plain desktop stream has no game."
          },
          "paired_clients": {
            "type": "integer",
            "format": "int32",
            "description": "Number of pinned (paired) client certificates.",
            "minimum": 0
          },
          "pin_pending": {
            "type": "boolean",
            "description": "True while a pairing handshake is parked waiting for the user's PIN\n(submit it via `POST /api/v1/pair/pin`)."
          },
          "session": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/SessionInfo",
                "description": "A representative active session. GameStream's launch (Moonlight `/launch`) when present, else\nthe first live native session. `null` when nothing is streaming."
              }
            ]
          },
          "stream": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/StreamInfo",
                "description": "The active stream's parameters — RTSP-negotiated for GameStream, or the live native session's\nmode/codec/bitrate. `null` when nothing is streaming."
              }
            ]
          },
          "video_streaming": {
            "type": "boolean",
            "description": "True while the video stream thread is running."
          }
        }
      },
      "RuntimeView": {
        "type": "object",
        "required": [
          "installed",
          "enabled",
          "running",
          "unit"
        ],
        "properties": {
          "detail": {
            "type": [
              "string",
              "null"
            ]
          },
          "enabled": {
            "type": "boolean"
          },
          "installed": {
            "type": "boolean",
            "description": "Is the runner payload/unit present at all?"
          },
          "principal": {
            "type": [
              "string",
              "null"
            ],
            "description": "Windows: the account the task runs as."
          },
          "running": {
            "type": "boolean"
          },
          "unit": {
            "type": "string",
            "description": "systemd unit or scheduled-task name."
          }
        }
      },
      "ScannerInfo": {
        "type": "object",
        "description": "One installed-store scanner this host build supports, with its enable state — the unit the\nconsole renders a toggle for. The list is platform-gated at compile time (the scanners are),\nso the console never shows a toggle that cannot do anything on this host.",
        "required": [
          "id",
          "label",
          "enabled"
        ],
        "properties": {
          "enabled": {
            "type": "boolean",
            "description": "Whether this host runs the scanner (default true)."
          },
          "id": {
            "type": "string",
            "description": "Stable scanner id — the same string the scanner's entries carry in their `store` field.",
            "example": "steam"
          },
          "label": {
            "type": "string",
            "description": "Human-facing name for the console toggle.",
            "example": "Steam"
          }
        }
      },
      "ScannerToggle": {
        "type": "object",
        "description": "Request body for `setLibraryScanner`.",
        "required": [
          "enabled"
        ],
        "properties": {
          "enabled": {
            "type": "boolean",
            "description": "Whether the scanner should run on this host."
          }
        }
      },
      "SessionInfo": {
        "type": "object",
        "description": "Client-requested launch parameters (key material is never exposed here).",
        "required": [
          "width",
          "height",
          "fps"
        ],
        "properties": {
          "fps": {
            "type": "integer",
            "format": "int32",
            "minimum": 0
          },
          "height": {
            "type": "integer",
            "format": "int32",
            "minimum": 0
          },
          "width": {
            "type": "integer",
            "format": "int32",
            "minimum": 0
          }
        }
      },
      "SessionRef": {
        "type": "object",
        "description": "A live A/V session (the plane-neutral notion the Dashboard shows).",
        "required": [
          "id",
          "client",
          "mode",
          "hdr"
        ],
        "properties": {
          "client": {
            "type": "string",
            "description": "Short client label (cert-fingerprint prefix, or peer IP for an anonymous client)."
          },
          "hdr": {
            "type": "boolean"
          },
          "id": {
            "type": "integer",
            "format": "int64",
            "description": "Host-local session id (unique within this host process).",
            "minimum": 0
          },
          "mode": {
            "type": "string",
            "description": "Negotiated mode, `WxH@Hz` (e.g. `\"3840x2160@120\"`)."
          }
        }
      },
      "SessionSettings": {
        "type": "object",
        "description": "The persisted settings.",
        "properties": {
          "disconnect_grace_seconds": {
            "type": "integer",
            "format": "int32",
            "description": "How long a vanished client has to reconnect before `Always` ends its game. Ignored by the\nother two policies.",
            "minimum": 0
          },
          "game_on_session_end": {
            "$ref": "#/components/schemas/GameOnSessionEnd",
            "description": "End the launched game when the session ends. See [`GameOnSessionEnd`]."
          },
          "session_on_game_exit": {
            "type": "boolean",
            "description": "End the streaming session when the launched game exits."
          },
          "version": {
            "type": "integer",
            "format": "int32",
            "minimum": 0
          }
        }
      },
      "SessionSettingsState": {
        "type": "object",
        "description": "The session⇄game lifetime settings, plus which axes this build acts on.",
        "required": [
          "settings",
          "configured",
          "enforced"
        ],
        "properties": {
          "configured": {
            "type": "boolean",
            "description": "Whether an operator has ever saved these settings (`false` ⇒ `settings` are the defaults)."
          },
          "enforced": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Which fields this build actually enforces. Empty on a platform with no launch path (macOS),\nso the console can say so instead of offering a switch that does nothing."
          },
          "settings": {
            "$ref": "#/components/schemas/SessionSettings",
            "description": "The stored settings (or the built-in defaults when this host has never been configured)."
          }
        }
      },
      "SetGpuPreference": {
        "type": "object",
        "description": "Request body for `setGpuPreference`.",
        "required": [
          "mode"
        ],
        "properties": {
          "gpu_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "Required when `mode` is `manual`: the stable `id` of a currently listed GPU\n(see `listGpus`).",
            "example": "10de-2c05-0"
          },
          "mode": {
            "type": "string",
            "description": "`auto` (env pin, else max dedicated VRAM — the default) or `manual`.",
            "example": "manual"
          }
        }
      },
      "SourceInput": {
        "type": "object",
        "required": [
          "url"
        ],
        "properties": {
          "public_key": {
            "type": [
              "string",
              "null"
            ],
            "description": "`ed25519:<base64>`. Omitted ⇒ an unsigned source (accepted, flagged everywhere)."
          },
          "url": {
            "type": "string"
          }
        }
      },
      "SourceView": {
        "type": "object",
        "description": "A configured catalog source and how its last refresh went.",
        "required": [
          "name",
          "url",
          "builtin",
          "signed",
          "stale",
          "entry_count"
        ],
        "properties": {
          "builtin": {
            "type": "boolean",
            "description": "The built-in `unom` source: not editable, not removable, and the only source whose entries\nmay carry the \"verified\" tier."
          },
          "entry_count": {
            "type": "integer",
            "format": "int32",
            "minimum": 0
          },
          "error": {
            "type": [
              "string",
              "null"
            ],
            "description": "Why the last refresh failed, if it did."
          },
          "fetched_at": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64",
            "description": "Unix seconds of the data we hold, when we hold any.",
            "minimum": 0
          },
          "name": {
            "type": "string"
          },
          "public_key": {
            "type": [
              "string",
              "null"
            ]
          },
          "signed": {
            "type": "boolean",
            "description": "Whether we check a signature on this source's index. An unsigned source still works; the\nconsole marks it."
          },
          "stale": {
            "type": "boolean",
            "description": "The catalog we're serving is older than the last refresh attempt (offline, or the last\nfetch failed) — entries still install, because the pin travelled with the entry."
          },
          "url": {
            "type": "string"
          }
        }
      },
      "StageTiming": {
        "type": "object",
        "description": "One pipeline stage's latency in an aggregation window (microseconds).",
        "required": [
          "name",
          "p50_us",
          "p99_us"
        ],
        "properties": {
          "name": {
            "type": "string",
            "description": "`\"capture\" | \"submit\" | \"encode\" | \"packetize\" | \"send\"` (path-dependent)."
          },
          "p50_us": {
            "type": "number",
            "format": "float"
          },
          "p99_us": {
            "type": "number",
            "format": "float"
          }
        }
      },
      "State": {
        "type": "string",
        "enum": [
          "running",
          "done",
          "failed"
        ]
      },
      "StatsSample": {
        "type": "object",
        "description": "One aggregated sample (~ every 2 s native, ~ every 1 s GameStream).",
        "required": [
          "t_ms",
          "session_id",
          "stages",
          "fps",
          "repeat_fps",
          "mbps",
          "bitrate_kbps",
          "frames_dropped",
          "packets_dropped",
          "send_dropped",
          "fec_recovered"
        ],
        "properties": {
          "bitrate_kbps": {
            "type": "integer",
            "format": "int32",
            "description": "Configured target bitrate.",
            "minimum": 0
          },
          "fec_recovered": {
            "type": "integer",
            "format": "int32",
            "description": "FEC shards recovered this window (delta).",
            "minimum": 0
          },
          "fps": {
            "type": "number",
            "format": "float",
            "description": "Genuine NEW frames/s from the source."
          },
          "frames_dropped": {
            "type": "integer",
            "format": "int32",
            "description": "Frames dropped this window (delta).",
            "minimum": 0
          },
          "mbps": {
            "type": "number",
            "format": "float",
            "description": "Attempted sealed wire bytes/s (Mb/s): full UDP payloads at seal time — video AU bytes\nplus shard framing (header + AEAD) plus FEC parity, and for PyroWave's datagram-aligned\nmode the zero-padded window tails. NOT goodput, and NOT reduced by socket send drops."
          },
          "packets_dropped": {
            "type": "integer",
            "format": "int32",
            "description": "Packets dropped this window (receiver-side / reassembler, where known).",
            "minimum": 0
          },
          "repeat_fps": {
            "type": "number",
            "format": "float",
            "description": "Re-encoded holds/s (source-starvation indicator)."
          },
          "send_dropped": {
            "type": "integer",
            "format": "int32",
            "description": "Host send-buffer overflow / EAGAIN this window (delta).",
            "minimum": 0
          },
          "session_id": {
            "type": "integer",
            "format": "int32",
            "description": "Disambiguates concurrent sessions (usually constant).",
            "minimum": 0
          },
          "stages": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/StageTiming"
            },
            "description": "Ordered pipeline stages for this path."
          },
          "t_ms": {
            "type": "integer",
            "format": "int64",
            "description": "Milliseconds since capture start (monotonic; stamped by [`StatsRecorder::push_sample`]).",
            "minimum": 0
          }
        }
      },
      "StatsStatus": {
        "type": "object",
        "description": "Snapshot of the in-progress capture for the management API.",
        "required": [
          "armed",
          "sample_count",
          "started_unix_ms",
          "elapsed_ms",
          "kind"
        ],
        "properties": {
          "armed": {
            "type": "boolean",
            "description": "Capture currently running."
          },
          "elapsed_ms": {
            "type": "integer",
            "format": "int64",
            "description": "Host-measured elapsed time of the in-progress capture, in ms (`0` if idle). Computed from the\nhost's MONOTONIC clock, so a console can show elapsed time without subtracting `started_unix_ms`\nfrom its own (possibly skewed) wall clock.",
            "minimum": 0
          },
          "kind": {
            "type": "string",
            "description": "Path of the in-progress capture (`\"\"` if idle)."
          },
          "sample_count": {
            "type": "integer",
            "format": "int32",
            "description": "Samples in the in-progress capture.",
            "minimum": 0
          },
          "started_unix_ms": {
            "type": "integer",
            "format": "int64",
            "description": "Unix start time of the in-progress capture (`0` if idle).",
            "minimum": 0
          }
        }
      },
      "StreamInfo": {
        "type": "object",
        "description": "RTSP-negotiated stream parameters.",
        "required": [
          "width",
          "height",
          "fps",
          "bitrate_kbps",
          "packet_size",
          "min_fec",
          "codec"
        ],
        "properties": {
          "bitrate_kbps": {
            "type": "integer",
            "format": "int32",
            "minimum": 0
          },
          "codec": {
            "$ref": "#/components/schemas/ApiCodec"
          },
          "fps": {
            "type": "integer",
            "format": "int32",
            "minimum": 0
          },
          "height": {
            "type": "integer",
            "format": "int32",
            "minimum": 0
          },
          "last_resize_ms": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int32",
            "description": "Most recent mid-stream resize total, reconfigure → pipeline rebuilt, in ms (native sessions;\n`null` when no resize happened / GameStream).",
            "minimum": 0
          },
          "min_fec": {
            "type": "integer",
            "format": "int32",
            "description": "Client's parity floor per FEC block (`minRequiredFecPackets`).",
            "minimum": 0
          },
          "packet_size": {
            "type": "integer",
            "format": "int32",
            "description": "Video payload size per packet (bytes).",
            "minimum": 0
          },
          "time_to_first_frame_ms": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int32",
            "description": "Session bring-up total, hello → first video packet, in ms (native sessions; `null` on the\nGameStream plane or while the session is still bringing up).",
            "minimum": 0
          },
          "width": {
            "type": "integer",
            "format": "int32",
            "minimum": 0
          }
        }
      },
      "StreamRef": {
        "type": "object",
        "description": "A live video stream (what the stream marker file reflects).",
        "required": [
          "mode",
          "hdr",
          "client",
          "plane"
        ],
        "properties": {
          "app": {
            "type": [
              "string",
              "null"
            ],
            "description": "The launched app/title for this stream, when one was requested (store-qualified id on\nthe native plane, app title on the GameStream plane)."
          },
          "client": {
            "type": "string",
            "description": "Client-supplied device name; may be empty."
          },
          "hdr": {
            "type": "boolean"
          },
          "mode": {
            "type": "string",
            "description": "Negotiated mode, `WxH@Hz`."
          },
          "plane": {
            "$ref": "#/components/schemas/Plane"
          }
        }
      },
      "SubmitPin": {
        "type": "object",
        "description": "The PIN Moonlight displays during pairing.",
        "required": [
          "pin"
        ],
        "properties": {
          "pin": {
            "type": "string",
            "description": "1–16 ASCII digits (Moonlight shows 4).",
            "example": "1234"
          }
        }
      },
      "Topology": {
        "type": "string",
        "description": "What the host does to the box's display topology while managed virtual displays are up.",
        "enum": [
          "auto",
          "extend",
          "primary",
          "exclusive"
        ]
      },
      "UiCredential": {
        "type": "object",
        "description": "`GET /plugins/{id}/ui-credential` — the console proxy's server-side lookup (bearer + loopback).\nThis is the only endpoint that returns a secret; the console BFF denylists it from the browser.",
        "required": [
          "port",
          "secret"
        ],
        "properties": {
          "port": {
            "type": "integer",
            "format": "int32",
            "minimum": 0
          },
          "secret": {
            "type": "string"
          }
        }
      },
      "UninstallRequest": {
        "type": "object",
        "required": [
          "pkg"
        ],
        "properties": {
          "pkg": {
            "type": "string"
          }
        }
      }
    },
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer"
      }
    }
  },
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "tags": [
    {
      "name": "host",
      "description": "Host identity, capabilities, and liveness"
    },
    {
      "name": "gpu",
      "description": "GPU inventory and selection: list the host's GPUs, choose automatic or a preferred GPU, see the one in use"
    },
    {
      "name": "display",
      "description": "Virtual-display management policy: lifecycle (keep-alive), topology (primary/exclusive), conflict handling, identity, and layout"
    },
    {
      "name": "clients",
      "description": "Paired Moonlight client management"
    },
    {
      "name": "pairing",
      "description": "Pairing PIN delivery (the out-of-band half of the GameStream pairing handshake)"
    },
    {
      "name": "native",
      "description": "Native punktfunk/1 pairing: arm a window, display the host PIN, manage paired devices"
    },
    {
      "name": "session",
      "description": "Active streaming session control"
    },
    {
      "name": "library",
      "description": "Game library: installed-store titles (Steam) plus user-curated custom entries"
    },
    {
      "name": "stats",
      "description": "Streaming performance-stats capture: arm/stop a recording, read the live + saved time-series for graphing"
    },
    {
      "name": "logs",
      "description": "Host log stream: the newest in-memory log entries, cursor-paged for live following"
    },
    {
      "name": "events",
      "description": "Host lifecycle events: an SSE stream (client/session/stream lifecycle, pairing, displays, library, host) with Last-Event-ID resume and server-side kind filters"
    },
    {
      "name": "hooks",
      "description": "Operator hooks: commands and webhooks fired on lifecycle events (fire-and-forget — hooks observe, never veto)"
    },
    {
      "name": "plugins",
      "description": "Plugin directory: running `punktfunk-plugin-*` processes register a lease and, optionally, a loopback UI the web console proxies and adds to its nav"
    },
    {
      "name": "store",
      "description": "Plugin store: browse signed catalogs (verified first-party entries, attributed third-party sources), install/uninstall as tracked jobs, and switch the plugin runner on"
    }
  ]
}
