refactor(protocol): remove plugin activation wait

This commit is contained in:
Dax Raad
2026-09-13 18:03:18 -04:00
parent ce56111a4c
commit 8905af5074
18 changed files with 14 additions and 527 deletions
+11 -7
View File
@@ -1,7 +1,7 @@
# V2 HTTP API audit checklist
**Source:** `packages/protocol/openapi.json`
**Current endpoint count:** 140
**Current endpoint count:** 139
**Last regenerated:** 2026-09-13
## How to use this checklist
@@ -29,7 +29,7 @@ Review endpoints in document order. For each endpoint, select one disposition an
## Progress
- [x] Group 1: Foundation and placement (4)
- [ ] Group 2: Configuration and capability catalogs (17)
- [ ] Group 2: Configuration and capability catalogs (16)
- [ ] Group 3: Credentials, integrations, MCP, and web search (22)
- [ ] Group 4: Session lifecycle (12)
- [ ] Group 5: Session execution and inputs (11)
@@ -41,6 +41,11 @@ Review endpoints in document order. For each endpoint, select one disposition an
## Resolved during audit
### [x] `POST /api/plugin/await-activation`
- **Decision:** Remove
- **Notes:** Activation timing is an internal server concern. Catalog reads remain non-blocking.
### [x] Location response wrappers
- **Decision:** Reduce generic endpoint response locations to `{ directory }`.
@@ -76,14 +81,13 @@ Review endpoints in document order. For each endpoint, select one disposition an
## Group 2: Configuration and capability catalogs
**Endpoints:** 17
**Endpoints:** 16
| Done | Method | Path | Operation ID | Decision | Notes |
|---|---|---|---|---|---|
| [ ] 008 | `GET` | `/api/agent` | `agent.list` | | |
| [ ] 009 | `GET` | `/api/agent/{agentID}` | `agent.get` | | |
| [ ] 010 | `GET` | `/api/plugin` | `plugin.list` | | |
| [ ] 011 | `POST` | `/api/plugin/await-activation` | `plugin.awaitActivation` | | |
| [x] 008 | `GET` | `/api/agent` | `agent.list` | Keep | Request and response accepted as-is. |
| [x] 009 | `GET` | `/api/agent/{agentID}` | `agent.get` | Keep | Request, response, and not-found error accepted as-is. |
| [x] 010 | `GET` | `/api/plugin` | `plugin.list` | Keep | Request and response accepted as-is. |
| [ ] 012 | `POST` | `/api/plugin/check` | `plugin.check` | | |
| [ ] 013 | `POST` | `/api/plugin/update` | `plugin.update` | | |
| [ ] 014 | `GET` | `/api/model` | `model.list` | | |
+1 -2
View File
@@ -408,8 +408,7 @@ function turnStart(messageID: string, slash: PreparedPrompt["slash"], skill: Ski
async function loadCatalog(client: OpenCodeClient, cwd: string): Promise<Catalog> {
const location = { directory: cwd }
await client.plugin.awaitActivation({ location })
// Some providers discover models in the background after activation has settled.
// Some providers discover models in the background after plugin startup begins.
const deadline = Date.now() + 5_000
let missing = "No models are available"
while (Date.now() < deadline) {
@@ -4,47 +4,6 @@ import { makeACPFixture, makeSession, secondModel, testModel } from "./service-f
import { flattenSelectOptions, requireSelectOption } from "./subprocess"
describe("acp service directory behavior", () => {
test("does not cache an available model before plugin activation settles", async () => {
const requested = Promise.withResolvers<void>()
const release = Promise.withResolvers<void>()
let ready = false
await using fixture = makeACPFixture({
fetch(request) {
requested.resolve()
if (request.path === "/api/plugin/await-activation") {
return release.promise.then(() => {
ready = true
return new Response(null, { status: 204 })
})
}
if (!ready && request.path === "/api/model") {
return Response.json({ data: [{ ...testModel, providerID: "ambient" }] })
}
if (!ready && request.path === "/api/model/default") {
return Response.json({ data: { ...testModel, providerID: "ambient" } })
}
if (request.path === "/api/session" && request.method === "POST") {
return Response.json({ data: { ...makeSession("ses_ready"), model: undefined } })
}
return undefined
},
})
const pending = fixture.service.newSession({ cwd: "/workspace", mcpServers: [] })
try {
await requested.promise
expect(fixture.requests.map((request) => request.path)).toEqual(["/api/plugin/await-activation"])
expect(fixture.requests[0]?.query["location[directory]"]).toBe("/workspace")
release.resolve()
expect(currentValue(await pending, "model")).toBe("test/test-model")
expect(
fixture.requests.find((request) => request.path === "/api/session" && request.method === "POST")?.body,
).toMatchObject({ model: { providerID: "test", id: "test-model" } })
} finally {
release.resolve()
await pending.catch(() => {})
}
})
test("creates sessions from a catalog shared by concurrent callers in the same cwd", async () => {
let created = 0
await using fixture = makeACPFixture({
@@ -69,7 +28,6 @@ describe("acp service directory behavior", () => {
expect(currentValue(first[0], "mode")).toBe("build")
expect(
[
"/api/plugin/await-activation",
"/api/model",
"/api/model/default",
"/api/agent",
@@ -86,7 +44,6 @@ describe("acp service directory behavior", () => {
["/workspace", "/other"],
["/workspace", "/other"],
["/workspace", "/other"],
["/workspace", "/other"],
])
expect(
fixture.requests
-1
View File
@@ -152,7 +152,6 @@ export function makeACPFixture(options: FixtureOptions = {}) {
const directory = request.query["location[directory]"] ?? "/workspace"
const location = { directory, project: { id: "global", directory } }
if (request.path === "/api/plugin/await-activation") return new Response(null, { status: 204 })
if (request.path === "/api/event") {
let controller: ReadableStreamDefaultController<Uint8Array> | undefined
return new Response(
-1
View File
@@ -18,7 +18,6 @@ describe("acp service", () => {
body: request.method === "GET" ? undefined : await request.json().catch(() => undefined),
})
const location = { directory: "/workspace", project: { id: "global", directory: "/workspace" } }
if (url.pathname === "/api/plugin/await-activation") return new Response(null, { status: 204 })
if (url.pathname === "/api/model") return Response.json({ location, data: [model] })
if (url.pathname === "/api/model/default") return Response.json({ location, data: model })
if (url.pathname === "/api/agent") return Response.json({ location, data: [agent] })
-7
View File
@@ -78,12 +78,6 @@ export type PluginListInput = { readonly location?: { readonly directory?: strin
export type PluginListOutput = { readonly location: Location.PublicRef; readonly data: ReadonlyArray<Plugin.Info> }
export type PluginListOperation<E = never> = (input?: PluginListInput) => Effect.Effect<PluginListOutput, E>
export type PluginAwaitActivationInput = { readonly location?: { readonly directory?: string | undefined } | undefined }
export type PluginAwaitActivationOutput = void
export type PluginAwaitActivationOperation<E = never> = (
input?: PluginAwaitActivationInput,
) => Effect.Effect<PluginAwaitActivationOutput, E>
export type PluginCheckInput = {
readonly location?: { readonly directory?: string | undefined } | undefined
readonly target?: string | undefined
@@ -100,7 +94,6 @@ export type PluginUpdateOperation<E = never> = (input: PluginUpdateInput) => Eff
export interface PluginApi<E = never> {
readonly list: PluginListOperation<E>
readonly awaitActivation: PluginAwaitActivationOperation<E>
readonly check: PluginCheckOperation<E>
readonly update: PluginUpdateOperation<E>
}
@@ -14,8 +14,6 @@ import type {
AgentGetOutput,
PluginListInput,
PluginListOutput,
PluginAwaitActivationInput,
PluginAwaitActivationOutput,
PluginCheckInput,
PluginCheckOutput,
PluginUpdateInput,
@@ -322,11 +320,6 @@ const EndpointPluginList = (raw: RawClient["server.plugin"]) => (input?: PluginL
raw["plugin.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)),
)
const EndpointPluginAwaitActivation = (raw: RawClient["server.plugin"]) => (input?: PluginAwaitActivationInput) =>
preserveEffect<PluginAwaitActivationOutput>()(
raw["plugin.awaitActivation"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)),
)
const EndpointPluginCheck = (raw: RawClient["server.plugin"]) => (input?: PluginCheckInput) =>
preserveEffect<PluginCheckOutput>()(
raw["plugin.check"]({ query: { location: input?.["location"] }, payload: { target: input?.["target"] } }).pipe(
@@ -343,7 +336,6 @@ const EndpointPluginUpdate = (raw: RawClient["server.plugin"]) => (input: Plugin
const adaptGroupPlugin = (raw: RawClient["server.plugin"]) => ({
list: EndpointPluginList(raw),
awaitActivation: EndpointPluginAwaitActivation(raw),
check: EndpointPluginCheck(raw),
update: EndpointPluginUpdate(raw),
})
@@ -8,8 +8,6 @@ import type {
AgentGetOutput,
PluginListInput,
PluginListOutput,
PluginAwaitActivationInput,
PluginAwaitActivationOutput,
PluginCheckInput,
PluginCheckOutput,
PluginUpdateInput,
@@ -467,18 +465,6 @@ export function make(options: ClientOptions) {
},
requestOptions,
),
awaitActivation: (input?: PluginAwaitActivationInput, requestOptions?: RequestOptions) =>
request<PluginAwaitActivationOutput>(
{
method: "POST",
path: `/api/plugin/await-activation`,
query: { location: input?.["location"] },
successStatus: 204,
declaredStatuses: [400, 401],
empty: true,
},
requestOptions,
),
check: (input?: PluginCheckInput, requestOptions?: RequestOptions) =>
request<PluginCheckOutput>(
{
@@ -2625,12 +2625,6 @@ export type PluginListInput = {
export type PluginListOutput = { location: LocationPublicRef; data: Array<PluginInfo> }
export type PluginAwaitActivationInput = {
readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
}
export type PluginAwaitActivationOutput = void
export type PluginCheckInput = {
readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly target?: { readonly target?: string | undefined }["target"]
-66
View File
@@ -394,72 +394,6 @@
"summary": "List plugins"
}
},
"/api/plugin/await-activation": {
"post": {
"tags": ["plugin"],
"operationId": "plugin.awaitActivation",
"parameters": [
{
"name": "location",
"in": "query",
"schema": {
"anyOf": [
{
"type": "object",
"properties": {
"directory": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
}
},
"additionalProperties": false
},
{
"type": "null"
}
]
},
"required": false,
"style": "deepObject",
"explode": true
}
],
"security": [],
"responses": {
"204": {
"description": "<No Content>"
},
"400": {
"description": "InvalidRequestError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvalidRequestErrorEncoded"
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedErrorEncoded"
}
}
}
}
},
"description": "Wait for configured plugin activation at a Location to settle, including missing-package installs. Completion does not imply every plugin succeeded or background resource discovery finished. Cancelling this wait does not cancel activation.",
"summary": "Wait for plugin activation"
}
},
"/api/plugin/check": {
"post": {
"tags": ["plugin"],
-15
View File
@@ -20,21 +20,6 @@ export const PluginGroup = HttpApiGroup.make("server.plugin")
}),
),
)
.add(
HttpApiEndpoint.post("plugin.awaitActivation", "/api/plugin/await-activation", {
query: LocationQuery,
success: HttpApiSchema.NoContent,
})
.annotateMerge(locationQueryOpenApi)
.annotateMerge(
OpenApi.annotations({
identifier: "plugin.awaitActivation",
summary: "Wait for plugin activation",
description:
"Wait for configured plugin activation at a Location to settle, including missing-package installs. Completion does not imply every plugin succeeded or background resource discovery finished. Cancelling this wait does not cancel activation.",
}),
),
)
.add(
HttpApiEndpoint.post("plugin.check", "/api/plugin/check", {
query: LocationQuery,
-3
View File
@@ -13,11 +13,9 @@ export const PluginHandler = HttpApiBuilder.group(Api, "server.plugin", (handler
return yield* response(Plugin.Service.use((plugin) => plugin.list()))
}),
)
.handle("plugin.awaitActivation", () => Plugin.awaitActivation)
.handle("plugin.check", (ctx) =>
Effect.gen(function* () {
const plugins = yield* Plugin.Service
yield* plugins.awaitActivation
const inventory = yield* plugins.list()
const targets = [
...new Set(inventory.flatMap((plugin) => (plugin.source.type === "package" ? [plugin.source.target] : []))),
@@ -57,7 +55,6 @@ export const PluginHandler = HttpApiBuilder.group(Api, "server.plugin", (handler
.handle("plugin.update", (ctx) =>
Effect.gen(function* () {
const plugins = yield* Plugin.Service
yield* plugins.awaitActivation
const inventory = new Set(
(yield* plugins.list()).flatMap((plugin) => (plugin.source.type === "package" ? [plugin.source.target] : [])),
)
@@ -1,192 +0,0 @@
import { expect } from "bun:test"
import { mkdir } from "node:fs/promises"
import path from "node:path"
import { SdkPlugins } from "@opencode/core/plugin/sdk"
import { Plugin } from "@opencode/plugin/effect"
import { Context, Deferred, Effect, Fiber, Layer } from "effect"
import { HttpEffect, HttpRouter, HttpServer } from "effect/unstable/http"
import { tmpdirScoped } from "../../core/test/fixture/tmpdir"
import { it } from "../../core/test/lib/effect"
import { createRoutes } from "../src/routes"
const fixture = Effect.fn(function* (plugin: Plugin.Plugin) {
const tmp = yield* tmpdirScoped("opencode-plugin-activation-")
const first = path.join(tmp.path, "first")
const second = path.join(tmp.path, "second")
const config = path.join(tmp.path, "config")
yield* Effect.promise(() => Promise.all([first, second, config].map((directory) => mkdir(directory))))
const context = yield* Layer.build(
createRoutes({
password: "secret",
database: { path: ":memory:" },
models: { fetch: false },
fs: { filewatcher: false },
config: {
directory: config,
project: false,
content: JSON.stringify({
providers: {
acme: {
models: {
reasoner: { name: "Configured Reasoner", limit: { context: 96_000, output: 8_000 } },
},
},
},
}),
},
}).pipe(Layer.provide(HttpServer.layerServices)),
)
const sdk = Context.get(context, SdkPlugins.Service)
yield* sdk.register(plugin)
const handler = Context.get(context, HttpRouter.HttpRouter).asHttpEffect().pipe(HttpEffect.toWebHandlerWith(context))
return {
first,
second,
request: (method: "GET" | "POST", route: string, directory = first, signal?: AbortSignal) =>
Effect.promise((interruption) => {
const url = new URL(route, "http://opencode.local")
url.searchParams.set("location[directory]", directory)
return handler(
new Request(url, {
method,
headers: { authorization: `Basic ${btoa("opencode:secret")}` },
signal: signal ?? interruption,
}),
)
}),
}
})
it.live(
"awaits activation only for the requested location without blocking model or plugin snapshots",
() =>
Effect.gen(function* () {
const started = yield* Deferred.make<void>()
const release = yield* Deferred.make<void>()
const server = yield* fixture(
Plugin.define({
id: "slow-plugin",
effect: (ctx) =>
Effect.gen(function* () {
if (path.basename(ctx.location.directory) !== "first") return
yield* Deferred.succeed(started, undefined)
yield* Deferred.await(release)
}),
}),
)
const pending = yield* server.request("POST", "/api/plugin/await-activation").pipe(Effect.forkScoped)
yield* Deferred.await(started)
expect(pending.pollUnsafe()).toBeUndefined()
const models = yield* server.request("GET", "/api/model")
expect(models.status).toBe(200)
expect(yield* Effect.promise(() => models.json())).toMatchObject({
location: { directory: server.first },
data: expect.not.arrayContaining([expect.objectContaining({ providerID: "acme", id: "reasoner" })]),
})
const plugins = yield* server.request("GET", "/api/plugin")
expect(plugins.status).toBe(200)
expect(yield* Effect.promise(() => plugins.json())).toMatchObject({ location: { directory: server.first } })
const second = yield* server.request("POST", "/api/plugin/await-activation", server.second)
expect(second.status).toBe(204)
expect(pending.pollUnsafe()).toBeUndefined()
yield* Deferred.succeed(release, undefined)
const response = yield* Fiber.join(pending)
expect(response.status).toBe(204)
expect(yield* Effect.promise(() => response.text())).toBe("")
const configured = yield* server.request("GET", "/api/model")
expect(configured.status).toBe(200)
expect(yield* Effect.promise(() => configured.json())).toMatchObject({
location: { directory: server.first },
data: expect.arrayContaining([
expect.objectContaining({
providerID: "acme",
id: "reasoner",
name: "Configured Reasoner",
limit: { context: 96_000, output: 8_000 },
}),
]),
})
const active = yield* server.request("GET", "/api/plugin")
expect(active.status).toBe(200)
expect(yield* Effect.promise(() => active.json())).toMatchObject({
data: expect.arrayContaining([
expect.objectContaining({ id: "slow-plugin", source: { type: "sdk" }, state: { status: "active" } }),
]),
})
}).pipe(Effect.timeout("10 seconds")),
15_000,
)
it.live(
"aborting an activation wait does not cancel plugin setup",
() =>
Effect.gen(function* () {
const started = yield* Deferred.make<void>()
const release = yield* Deferred.make<void>()
const completed = yield* Deferred.make<void>()
const interrupted = yield* Deferred.make<void>()
const server = yield* fixture(
Plugin.define({
id: "slow-plugin",
effect: () =>
Effect.gen(function* () {
yield* Deferred.succeed(started, undefined)
yield* Deferred.await(release)
yield* Deferred.succeed(completed, undefined)
}).pipe(Effect.onInterrupt(() => Deferred.succeed(interrupted, undefined))),
}),
)
const controller = new AbortController()
yield* Effect.addFinalizer(() => Effect.sync(() => controller.abort()))
const pending = yield* server
.request("POST", "/api/plugin/await-activation", server.first, controller.signal)
.pipe(Effect.forkScoped)
yield* Deferred.await(started)
controller.abort()
// HttpEffect resolves a cancelled Web request with 499 rather than rejecting its Promise.
expect((yield* Fiber.join(pending)).status).toBe(499)
expect(yield* Deferred.isDone(interrupted)).toBe(false)
expect(yield* Deferred.isDone(completed)).toBe(false)
yield* Deferred.succeed(release, undefined)
expect((yield* server.request("POST", "/api/plugin/await-activation")).status).toBe(204)
expect(yield* Deferred.isDone(completed)).toBe(true)
expect(yield* Deferred.isDone(interrupted)).toBe(false)
const plugins = yield* server.request("GET", "/api/plugin")
expect(plugins.status).toBe(200)
expect(yield* Effect.promise(() => plugins.json())).toMatchObject({
data: expect.arrayContaining([expect.objectContaining({ id: "slow-plugin", state: { status: "active" } })]),
})
}).pipe(Effect.timeout("10 seconds")),
15_000,
)
it.live(
"settles activation when plugin setup fails and exposes the failure in the inventory",
() =>
Effect.gen(function* () {
const server = yield* fixture(
Plugin.define({
id: "failing-plugin",
effect: () => Effect.die(new Error("fixture setup failed")),
}),
)
expect((yield* server.request("POST", "/api/plugin/await-activation")).status).toBe(204)
const plugins = yield* server.request("GET", "/api/plugin")
expect(plugins.status).toBe(200)
expect(yield* Effect.promise(() => plugins.json())).toMatchObject({
location: { directory: server.first },
data: expect.arrayContaining([
expect.objectContaining({
id: "failing-plugin",
source: { type: "sdk" },
state: { status: "failed", error: expect.stringContaining("fixture setup failed") },
}),
]),
})
}).pipe(Effect.timeout("10 seconds")),
15_000,
)
+2 -26
View File
@@ -1,7 +1,7 @@
import { expect } from "bun:test"
import { SdkPlugins } from "@opencode/core/plugin/sdk"
import { Plugin } from "@opencode/plugin/effect"
import { Context, Deferred, Effect, Fiber, Layer } from "effect"
import { Context, Deferred, Effect, Layer } from "effect"
import { HttpEffect, HttpRouter, HttpServer } from "effect/unstable/http"
import { tmpdirScoped } from "../../core/test/fixture/tmpdir"
import { it } from "../../core/test/lib/effect"
@@ -62,11 +62,9 @@ it.live(
}),
)
})
const pending = yield* request("POST", "/api/plugin/await-activation").pipe(Effect.forkScoped)
yield* Deferred.await(started)
// Config providers activate after SDK plugins; reads must return the current snapshot without waiting.
const list = yield* request("GET", "/api/provider").pipe(Effect.timeout("2 seconds"))
yield* Deferred.await(started)
expect(list.status).toBe(200)
expect(yield* Effect.promise(() => list.json())).toMatchObject({
location: { directory: tmp.path },
@@ -78,29 +76,7 @@ it.live(
_tag: "ProviderNotFoundError",
providerID: "custom",
})
expect(pending.pollUnsafe()).toBeUndefined()
yield* Deferred.succeed(release, undefined)
expect((yield* Fiber.join(pending)).status).toBe(204)
const provider = {
id: "custom",
name: "Configured Custom Provider",
activation: "enabled",
package: "@opencode/ai/providers/openai-compatible",
settings: { apiKey: "secret" },
}
const configuredList = yield* request("GET", "/api/provider").pipe(Effect.timeout("2 seconds"))
expect(configuredList.status).toBe(200)
expect(yield* Effect.promise(() => configuredList.json())).toMatchObject({
location: { directory: tmp.path },
data: expect.arrayContaining([expect.objectContaining(provider)]),
})
const configuredGet = yield* request("GET", "/api/provider/custom").pipe(Effect.timeout("2 seconds"))
expect(configuredGet.status).toBe(200)
expect(yield* Effect.promise(() => configuredGet.json())).toMatchObject({
location: { directory: tmp.path },
data: provider,
})
}),
15_000,
)
-3
View File
@@ -215,9 +215,6 @@ it.live(
const api = OpenCode.make({ baseUrl: server.base, headers: server.headers })
yield* Effect.promise(async () => {
await api.location.get({ location: { directory: source } })
const url = new URL("/api/plugin/await-activation", server.base)
url.searchParams.set("location[directory]", source)
expect((await fetch(url, { method: "POST", headers: server.headers })).status).toBe(204)
expect(await api.worktree.list({ location: { directory: target } })).toContainEqual({
directory: path.join(destination, "delegated"),
strategy: "target-copy",
@@ -148,7 +148,6 @@ export function PluginsDialog(props: {
location,
targets: [entry.plugin.source.target],
})
.then(() => props.context.client.plugin.awaitActivation({ location }))
.then(() => refetch())
.catch((cause) => {
props.context.ui.toast.show({
-66
View File
@@ -394,72 +394,6 @@
"summary": "List plugins"
}
},
"/api/plugin/await-activation": {
"post": {
"tags": ["plugin"],
"operationId": "plugin.awaitActivation",
"parameters": [
{
"name": "location",
"in": "query",
"schema": {
"anyOf": [
{
"type": "object",
"properties": {
"directory": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
}
},
"additionalProperties": false
},
{
"type": "null"
}
]
},
"required": false,
"style": "deepObject",
"explode": true
}
],
"security": [],
"responses": {
"204": {
"description": "<No Content>"
},
"400": {
"description": "InvalidRequestError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvalidRequestErrorEncoded"
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedErrorEncoded"
}
}
}
}
},
"description": "Wait for configured plugin activation at a Location to settle, including missing-package installs. Completion does not imply every plugin succeeded or background resource discovery finished. Cancelling this wait does not cancel activation.",
"summary": "Wait for plugin activation"
}
},
"/api/plugin/check": {
"post": {
"tags": ["plugin"],
-66
View File
@@ -394,72 +394,6 @@
"summary": "List plugins"
}
},
"/api/plugin/await-activation": {
"post": {
"tags": ["plugin"],
"operationId": "plugin.awaitActivation",
"parameters": [
{
"name": "location",
"in": "query",
"schema": {
"anyOf": [
{
"type": "object",
"properties": {
"directory": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
}
},
"additionalProperties": false
},
{
"type": "null"
}
]
},
"required": false,
"style": "deepObject",
"explode": true
}
],
"security": [],
"responses": {
"204": {
"description": "<No Content>"
},
"400": {
"description": "InvalidRequestError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvalidRequestErrorEncoded"
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedErrorEncoded"
}
}
}
}
},
"description": "Wait for configured plugin activation at a Location to settle, including missing-package installs. Completion does not imply every plugin succeeded or background resource discovery finished. Cancelling this wait does not cancel activation.",
"summary": "Wait for plugin activation"
}
},
"/api/plugin/check": {
"post": {
"tags": ["plugin"],