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

  1. Verify the commandId format before invoking: it must have at least 3 dot segments.
  2. Reference the exact id declared in the callee's manifest contributes.commands[].id.
  3. If the id is user/template-derived, validate it against the format guard below and reject early with a clear caller-side error.
  4. 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

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


AI-assisted analysis of agalwood/Motrix@1a708ee577 (2026-08-12). Data as JSON: /api/errors/3fa5323d158f4026. Report an issue: GitHub.