agalwood/Motrix · error · AppError
PluginRuntimeFault
PluginRuntimeFault
Error message
plugin.command.access_denied
What it means
Thrown at the very top of FullCrossPluginInvoker.execute when parseCalleeId(commandId) returns undefined — i.e. the command id does not contain at least three dot-separated segments (plugin.namespace.command). This early reject happens before the depth counter is entered and before any shared state is touched, so it is the cheapest possible access-denial path. The audit entry is logged with an empty callee and then AppError(PluginRuntimeFault) is raised.
Source
Thrown at src/core/plugin/commands/cross-plugin-invoker.ts:132
const taskId = this.taskIdProvider() ?? `_no_task_${callerId}_${startTs}`
const argsSize = measureBytes(args)
// Parse before entering the depth counter so a malformed commandId is
// a quick reject without touching shared state.
const parsed = parseCalleeId(commandId)
if (!parsed) {
const entry: CommandInvokeEntry = {
caller: callerId,
callee: '',
commandId,
argsSize,
durMs: Date.now() - startTs,
depth: this.depth.current(taskId),
ok: false,
errorCode: 'plugin.command.access_denied',
}
this.audit.log(entry)
throw new AppError(
ErrorCode.PluginRuntimeFault,
'plugin.command.access_denied'
)
}
const calleePluginId = parsed.pluginId
const depthValue = this.depth.enter(taskId)
const auditFail = (errorCode: string): never => {
this.audit.log({
caller: callerId,
callee: calleePluginId,
commandId,
argsSize,
durMs: Date.now() - startTs,
depth: depthValue,
ok: false,
errorCode,
})View on GitHub (pinned to 1a708ee577)
Solutions
- Verify the commandId format before invoking: it must have at least 3 dot segments.
- Reference the exact id declared in the callee's manifest contributes.commands[].id.
- If the id is user/template-derived, validate it against the format guard below and reject early with a clear caller-side error.
- Add the corrected commandId to the caller manifest's invokesCommands array (required even for well-formed ids — see the later access check).
Example fix
// before
await invoker.execute(callerId, 'download', args)
// after
const commandId = 'acme.fetcher.download'
if (!/^\w+\.\w+\.[\w.]+$/.test(commandId)) throw new Error('bad commandId')
await invoker.execute(callerId, commandId, args) Defensive patterns
Strategy: validation
Validate before calling
function isFullyQualifiedCommand(id: string): boolean { return id.split('.').length >= 3 } Type guard
function isFullyQualifiedCommand(id: string): id is string {
const parts = id.split('.')
return parts.length >= 3 && parts.every((p) => p.length > 0)
} Try / catch
try { await invoker.execute(callerId, commandId, args) }
catch (e) { if (e.message === 'plugin.command.access_denied') { /* validate commandId format/declares */ } else throw e } Prevention
- Build commandIds from validated manifest constants, not string concatenation.
- Ensure the caller's manifest invokesCommands lists the fully-qualified id.
- Reject user/template-derived ids that don't match the 3-segment shape.
When it happens
Trigger: Calling invoker.execute(callerId, commandId, args) with a commandId like 'foo', 'foo.bar', or any value whose split('.').length < 3. The expected shape is 'vendor.plugin.commandName'.
Common situations: A caller plugin hardcodes a command id with a typo (missing the command segment), passes a user-supplied string that wasn't validated, or a manifest references a command by a short alias instead of its fully-qualified id. Also occurs when dynamically building commandIds from template strings that yield undefined fields.
Related errors
- plugin.commands.access_denied
- 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/3fa5323d158f4026.
Report an issue: GitHub.