agalwood/Motrix · error · CommandsError
plugin.commands.access_denied
plugin.commands.access_denied
Error message
cross-plugin command "${commandId}" requires a bound invoker (Plan D) What it means
Thrown by CommandsCapabilityHost.execute() when the requested commandId does NOT start with the caller's own namespace (i.e. a foreign-namespace / cross-plugin call) and no CrossPluginInvoker was bound to the capability. Cross-plugin dispatch is intentionally delegated to a host-supplied invoker (the design note calls this 'Plan D'); without one the capability refuses rather than silently failing. The error code is `plugin.commands.access_denied`.
Source
Thrown at src/core/plugin/capabilities/commands.ts:191
durMs: Date.now() - startTs,
ok: true,
})
return result
} catch (err) {
this.onSelfInvoke?.({
callerId,
commandId,
durMs: Date.now() - startTs,
ok: false,
errorCode: err instanceof Error ? err.message : String(err),
})
throw err
}
}
// Cross-plugin path
if (!this.invoker) {
throw new CommandsError(
'plugin.commands.access_denied',
`cross-plugin command "${commandId}" requires a bound invoker (Plan D)`
)
}
return this.invoker.execute(callerId, commandId, args)
}
/**
* Remove all handlers whose command ID starts with `${callerId}.`.
* Used during plugin teardown.
*/
unregisterAll(callerId: string): void {
const prefix = `${callerId}.`
for (const key of this.handlers.keys()) {
if (key.startsWith(prefix)) {
this.handlers.delete(key)
}
}View on GitHub (pinned to 1a708ee577)
Solutions
- If cross-plugin calls are intended, wire the CrossPluginInvoker into CommandsCapabilityHost at construction (the host's plugin registry normally provides it).
- If the call was meant to stay in-namespace, prefix the commandId with the caller's own id so it dispatches locally.
- In tests, inject a stub invoker that records/forwards calls so the code path under test does not hit this guard.
- Confirm the host's capability-host.ts actually binds the invoker against the PluginRegistry before any plugin activate() runs.
Example fix
// before — no invoker bound, cross-plugin call fails
const cmds = new CommandsCapabilityHost({ /* invoker omitted */ })
await cmds.execute('pluginA', 'pluginB.cmd', {})
// after — bind an invoker (or restrict to own namespace)
const cmds = new CommandsCapabilityHost({
invoker: registry.crossInvoker, // implements CrossPluginInvoker
})
await cmds.execute('pluginA', 'pluginB.cmd', {}) Defensive patterns
Strategy: validation
Validate before calling
function canCallForeign(cmds: CommandsCapabilityHost, callerId: string, commandId: string): boolean {
return commandId.startsWith(`${callerId}.`) || !!getInvoker(cmds)
} Type guard
function isAccessDenied(e: unknown): e is CommandsError {
return e instanceof Error && (e as CommandsError).code === 'plugin.commands.access_denied'
} Try / catch
try {
await cmds.execute(callerId, foreignId, args)
} catch (e) {
if (isAccessDenied(e)) {
// degrade gracefully: own-namespace fallback or user-facing 'unsupported'
} else throw e
} Prevention
- Wire the CrossPluginInvoker during capability construction in the host bootstrap.
- In tests, inject a stub invoker rather than relying on cross-plugin routing.
- Document which commands are own-namespace vs foreign in the plugin manifest.
When it happens
Trigger: A plugin calls `execute('pluginA', 'pluginB.someCommand', args)` while `callerId='pluginA'` so the id is foreign, and the host constructed CommandsCapabilityHost without passing an invoker (or passed `undefined`). Common in unit tests of a plugin in isolation, or when the host hasn't wired the plugin registry's cross-invoker.
Common situations: Host/bootstrap code forgot to bind the CrossPluginInvoker during capability construction; plugin is being tested outside the full plugin host that normally supplies the invoker; a refactor removed the invoker wiring thinking own-namespace calls were the only path.
Related errors
- PluginRuntimeFault
- PluginRuntimeFault
- plugin.commands.id_out_of_namespace
- plugin.command.not_declared_in_manifest
- plugin.commands.not_found
AI-assisted analysis of agalwood/Motrix@1a708ee577 (2026-08-12).
Data as JSON: /api/errors/71d162602d792fa2.
Report an issue: GitHub.