Executing plugin operations
Discover and run allowlisted JSON operations on plugin hosts through your Remedia connection, without configuring plugin-host endpoints in your application.
Use client.plugins.execute() to request JSON from an operator-approved plugin host. The selected Remedia server holds the host endpoint and credentials. Your application supplies the node, host ID, operation, and input.
Allowlisting and discovery
An operation needs both authorization and a current advertisement:
| Concern | Where | Who decides |
|---|---|---|
| Allowlisting | pluginHosts[].operations in remedia.config.json | The server operator. Authoritative and never bypassed |
| Discovery | client.plugins.list() / plugin.list | Automatic. Reports what the operator allows and the host currently advertises |
Configure the plugin host only on the Remedia server:
{
"pluginHosts": [
{
"id": "source-host-1",
"url": "https://plugins.example.com",
"auth": { "type": "bearer", "token": "env('REMEDIA_PLUGIN_HOST_TOKEN')" },
"operations": [
{ "pluginId": "example.metadata", "operation": "catalog.search", "replaySafe": true },
{ "pluginId": "example.metadata", "operation": "lyrics.search", "replaySafe": true },
{ "pluginId": "example.source", "operation": "source.stream", "replaySafe": true }
]
}
]
}Only the intersection of the operator allowlist and the host advertisement is callable. See Permissions and security for network, retry, and credential rules, and host configuration for all limits.
Complete example: search, then play
The function below uses the initialized client from the client guide. The host defines catalog.search to return IDs and titles and source.stream to return encoded media for an ID.
import type { RemediaClient } from '@remedia/client';
type SearchResult = {
items: Array<{ id: string; title: string }>;
};
async function searchThenPlay(
client: RemediaClient,
guildId: string,
channelId: string,
userId: string,
query: string,
): Promise<void> {
const outcome = await client.plugins.execute<SearchResult>({
nodeId: 'media-1',
hostId: 'source-host-1',
pluginId: 'example.metadata',
operation: 'catalog.search',
input: { query },
});
if (!outcome.handled || outcome.result.items.length === 0) return;
const voice = client.createDiscordSession({
guildId,
channelId,
userId,
nodeId: outcome.nodeId,
pluginHosts: [outcome.hostId],
});
try {
await voice.connect();
await voice.play({
hostId: outcome.hostId,
pluginId: 'example.source',
operation: 'source.stream',
input: { id: outcome.result.items[0]!.id },
});
await voice.wait();
} finally {
await voice.dispose();
}
}The result generic describes your host's contract. Validate responses at your application boundary if the host is not trusted to implement that shape.
Cancel a forwarded request
Aborting the signal while the request is pending cancels it on the client and on the server: the HTTP request to the plugin host is dropped, the host's own operation signal aborts, and the host's concurrency permit is released.
Commands
All three commands require an authenticated connection with a client session (client.create or
client.resume). They do not create guild sessions. See the protocol reference
for the envelope.
plugin.list
Params: { "refresh"?: boolean }. Returns every configured host, ordered by hostId, with its
operations ordered by pluginId then operation:
{
"hosts": [
{
"hostId": "source-host-1",
"status": "healthy",
"operations": [
{ "pluginId": "example.metadata", "operation": "catalog.search" },
{ "pluginId": "example.metadata", "operation": "lyrics.search" },
{ "pluginId": "example.source", "operation": "source.stream" }
]
}
]
}operationsare the operations that are both allowlisted by the operator and advertised by the host's manifest. Allowed-but-unadvertised and advertised-but-unallowed operations are omitted.statusis the node's current view:healthy,degraded(recent failures),unavailable(repeated failures),unknown(never contacted) orshutting_down. A host which cannot be discovered or whose cached manifest cannot be refreshed is reported with no operations rather than a stale list.hostIdis always the configured ID, never a manifest display name.- Responses never include endpoint URLs, tokens, media grant URLs or other deployment details.
- Manifests are cached for
limits.discoveryTtlMs(default 60 s) and shared with playback. A host that restarts with a different operation set is picked up when the cache expires, when an execution discovers the operation is gone, or onrefresh: true, which is rate-limited to one refetch per five seconds per host. - A plugin host's manifest is public by design, so discovery succeeds even if the node's configured
token is wrong. Such a host is listed until an execution fails with
plugin_host_unauthorized, after which its status degrades.
plugin.execute
Params (unknown fields are rejected; input is required but may be null):
{
"hostId": "source-host-1",
"pluginId": "example.metadata",
"operation": "catalog.search",
"input": { "query": "some query" }
}The server authenticates the connection, validates the identifiers, locates exactly the named host, verifies that the operation is allowlisted and currently advertised, executes it through the shared HTTP client, and returns the plugin host's semantics unchanged:
{ "handled": true, "result": { "items": [] } }{ "handled": false }An unhandled operation is an ordinary result, never an error. null, false, 0, "" and []
are valid handled results and are distinct from handled: false.
There is no fallback: the request targets one host and never tries another host or node. Fan-out across providers is application logic.
The serialized { "handled": ..., "result": ... } envelope must fit in one RPC message
(limits.maxMessageBytes, default 64 KiB). A larger result is not sent. The request fails with
plugin_result_too_large and the connection stays usable. Raise maxMessageBytes (and the host's
maxResponseBytes) or return less data.
plugin.cancel
Params: { "requestId": "<id of a pending plugin.list or plugin.execute>" }. Result:
{ "cancelled": boolean }. It is handled even when all request slots are busy and it can only see the
calling connection's own pending requests, so a client can never cancel another client's
operation. @remedia/client sends it automatically when a signal aborts or a deadline passes.
Cancellation, deadlines and cleanup
A pending operation is cancelled, its HTTP request dropped and its permits released when:
- the client sends
plugin.cancel(abort or client deadline); - the WebSocket closes;
- the client session closes (
client.closeor expiry); - the server's
limits.requestTimeoutMselapses (the request fails withrequest_timeout); - the server shuts down.
Cancellation registries are per connection, contain only requests currently in flight, and are
bounded by limits.maxInFlightRequests. Nothing is retained after a timeout, reconnect or socket close.
Retries
Only operations marked replaySafe are retried, only for connection failures, timeouts and
408/425/429/5xx responses, up to limits.maxAttempts with a short pause between attempts.
Authentication failures, invalid requests, policy violations, malformed responses and
non-replay-safe operations are never retried.
Errors
| Code | Meaning |
|---|---|
plugin_host_not_found | hostId is not configured on this node |
plugin_operation_not_allowed | The operation is not in the host's allowlist |
plugin_operation_unavailable | Allowlisted, but the host does not currently advertise it |
plugin_host_unauthorized | The host rejected the configured credentials |
plugin_host_unavailable | Connection failure, DNS policy rejection or transient 429/5xx |
plugin_host_incompatible | Wrong protocol version, identity or routes, or a redirect |
plugin_operation_failed | The plugin failed, or returned a malformed or over-limit response |
plugin_result_too_large | The result does not fit in one RPC message |
invalid_request | Bad identifiers or fields, or input over maxRequestBytes |
resource_exhausted | The host's maxPendingOperations or the connection's request limit was hit |
unknown_session | The connection has no client session |
cancelled, request_timeout | Cancellation and deadline outcomes described above |
Messages never contain URLs, tokens or operation input or output.
Multiple nodes and affinity
Different Remedia nodes can have different plugin hosts, or different deployments under the same
host ID. @remedia/client therefore never guesses:
- An explicit
nodeIdis used as given. - Otherwise a
guildIdwith a sticky assignment uses the assigned node. A differentnodeIdfor that guild, or a lost assignment, is an error. Nothing is migrated. - Otherwise the call must be unambiguous: exactly one healthy node must advertise
hostId. Zero nodes or several nodes reject withNodeSelectionError.
Every handled or unhandled result carries the executing nodeId and hostId. Playing an operation
source later must use the same deployment, so pass nodeId (and pluginHosts) when creating the
Discord session, as in the example above.
Compatibility
- Protocol version stays at 1. The three commands are additive. Existing clients and playback are unaffected.
node.info.capabilities.pluginOperationsgainsforwarding: { available }(true when at least one host is configured) next to the existingavailableandhosts. Servers that predate forwarding omit it;@remedia/clientreportsPluginOperationsUnsupportedErrorinstead of sending commands that do not exist, and those servers keep working for operation-source playback.- Embedded nodes have no plugin-host registry and report forwarding as unavailable. Embedded integrations keep running their own locally registered plugins. No local HTTP server is started to imitate remote forwarding.
Observability
Executions appear in the existing plugin-host events (server.plugin_host.operation_completed and
operation_failed) with purpose (generic or media), host ID, plugin ID, operation, outcome,
attempts, retries, duration and a safe error category. Inputs and results are never logged.
node.stats.pluginHosts[].totals adds genericOperations, genericFailures and genericUnhandled.
Media-resolution counters (operations, operationFailures, grantsOpened, mediaBytes) and
resources.activeStreams are untouched by forwarded operations. retries, unauthorized, timeouts
and cancelled cover both kinds.