agalwood/Motrix · error · ConfigError

plugin.lifecycle.secrets_seed_missing

plugin.lifecycle.secrets_seed_missing

Error message

plugin "${this.opts.pluginId}" config key "${key}" is secret but no decryptSecret function was injected

What it means

Thrown by ConfigCapabilityHost's single-key getter when a config key is in `secretFields`, the resolved value is a non-empty string (stored value or schema default), but no `decryptSecret` function was supplied to the capability. The capability refuses to return ciphertext as plaintext, so a secret field without a decryption seed is treated as a misconfigured host. Code is `plugin.lifecycle.secrets_seed_missing`.

Source

Thrown at src/core/plugin/capabilities/config.ts:91

   * If the resolved value is a string and the key is secret, it is decrypted
   * via `decryptSecret`. If no decryptSecret is injected for a stored secret
   * string, throws ConfigError('plugin.lifecycle.secrets_seed_missing', ...).
   */
  async get(key: string): Promise<unknown> {
    const stored = this.opts.readValues()
    const storedValue = Object.hasOwn(stored, key) ? stored[key] : undefined

    const resolved =
      storedValue !== undefined ? storedValue : this.opts.schemaDefaults[key]

    // Only decrypt if the resolved value is a string and the key is secret.
    if (
      resolved !== undefined &&
      typeof resolved === 'string' &&
      this.opts.secretFields.has(key)
    ) {
      if (!this.opts.decryptSecret) {
        throw new ConfigError(
          'plugin.lifecycle.secrets_seed_missing',
          `plugin "${this.opts.pluginId}" config key "${key}" is secret but no decryptSecret function was injected`
        )
      }
      return this.opts.decryptSecret(resolved)
    }

    return resolved
  }

  /**
   * Returns the stored value verbatim — does NOT fall back to schema defaults
   * and does NOT decrypt. Intended for the renderer's "is this overridden?" check.
   */
  async getRaw(key: string): Promise<unknown> {
    const stored = this.opts.readValues()
    return Object.hasOwn(stored, key) ? stored[key] : undefined
  }

View on GitHub (pinned to 1a708ee577)

Solutions

  1. Inject a decryptSecret function into the Config capability options at host construction.
  2. If the field should not actually be secret, remove it from secretFields in the plugin's config schema.
  3. In dev/test, supply a no-op decryptor (returns input unchanged) only when values are already plaintext — never do this in production.
  4. Verify the secrets seed/bootstrap runs before plugin activate() and that the decryptor is bound per-plugin, not globally absent.

Example fix

// before
const config = new ConfigCapabilityHost({
  pluginId: 'p', secretFields: new Set(['apiToken']),
  readValues: () => ({ apiToken: cipherText }),
  // decryptSecret missing
})
await config.get('apiToken') // throws

// after
const config = new ConfigCapabilityHost({
  pluginId: 'p', secretFields: new Set(['apiToken']),
  readValues: () => ({ apiToken: cipherText }),
  decryptSecret: (ct) => kms.decrypt(ct),
})
Defensive patterns

Strategy: validation

Validate before calling

function assertDecryptor(opts: ConfigCapabilityHostOptions): void {
  if (opts.secretFields && opts.secretFields.size > 0 && !opts.decryptSecret) {
    throw new Error('decryptSecret required because secretFields is non-empty')
  }
}

Type guard

function isSecretsSeedMissing(e: unknown): e is ConfigError {
  return e instanceof Error && (e as ConfigError).code === 'plugin.lifecycle.secrets_seed_missing'
}

Try / catch

try {
  await config.get(key)
} catch (e) {
  if (isSecretsSeedMissing(e)) {
    // host misconfiguration: fail loudly in dev, alert ops in prod
  } else throw e
}

Prevention

When it happens

Trigger: Calling `config.get('apiToken')` where `apiToken` is declared secret in the schema and has a value, but the host built the Config capability without injecting `decryptSecret`. Also happens if the host secrets pipeline (e.g. a KMS/seed provider) was disabled in the current environment.

Common situations: Local dev environment where the secrets decryptor was never wired; CI runs with a stripped-down host config; a plugin manifest marks a field secret but the host hasn't provisioned the decryption seed for this plugin.

Related errors


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