chore(plugins): regenerate OpenAPI spec + SDK client for /api/v1/plugins
ci / web (push) Successful in 1m11s
ci / docs-site (push) Successful in 1m13s
apple / swift (push) Successful in 1m22s
decky / build-publish (push) Successful in 32s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 44s
ci / bench (push) Successful in 6m40s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 11s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 15s
apple / screenshots (push) Successful in 6m26s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 9m52s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 8m57s
deb / build-publish (push) Successful in 13m13s
windows-host / package (push) Successful in 15m54s
arch / build-publish (push) Successful in 17m25s
android / android (push) Successful in 18m9s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 15m26s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 19m50s
ci / rust (push) Successful in 27m22s
docker / deploy-docs (push) Successful in 23s

Regenerated api/openapi.json (host built on Linux) with the plugins tag —
registerPlugin/listPlugins/deregisterPlugin/getPluginUiCredential + the
PluginRegistration/PluginSummary/PluginUi/PluginUiPublic/UiCredential schemas —
and the SDK's generated client. Drift test + 44 mgmt/events host tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-18 01:56:08 +02:00
parent ec84b30eae
commit 2c0aee3979
2 changed files with 417 additions and 5 deletions
+329
View File
@@ -1931,6 +1931,184 @@
}
}
},
"/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": [
@@ -3294,6 +3472,25 @@
}
}
},
{
"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",
"required": [
@@ -4099,6 +4296,116 @@
"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 (164 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).",
@@ -4710,6 +5017,24 @@
"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"
}
}
}
},
"securitySchemes": {
@@ -4772,6 +5097,10 @@
{
"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"
}
]
}