moeru-ai/airi · error · Error

Extension entrypoint id

Error message

Extension entrypoint id `${extension.id}` must match manifest id `${options.manifest.id}`.

What it means

plugin-host's startExtension (packages/plugin-sdk/src/plugin-host/core.ts) requires that the Extension object your entrypoint exported has an id identical to the id declared in the ExtensionManifestV1 passed alongside it. The manifest id is the trust anchor for permission grants and session identity, so a mismatch between 'what the code claims to be' and 'what the manifest declares' is rejected outright. It throws before any session identity, permission resolution, or module registration happens.

Solutions

  1. Make the two ids byte-identical: open the manifest, copy its id field, and use it in the exported extension object (or derive the export from a shared constant).
  2. Better: have the entrypoint import/read the manifest id (or generate both from one source of truth) so they cannot drift.
  3. If you renamed the extension intentionally, update both the manifest id and the exported extension id (plus any persisted permission grants keyed by the old id).
  4. Add a unit test that loads the built manifest and asserts manifest.id === extension.id.

Example fix

// before
// manifest.json: { "id": "com.acme.weather", ... }
export const extension = defineExtension({ id: 'com.acme.wheather' }) // typo: mismatch

// after
import manifest from './manifest.json'
export const extension = defineExtension({ id: manifest.id })
Defensive patterns

Strategy: validation

Validate before calling

import manifest from './manifest.json' with { type: 'json' }
if (extension.id !== manifest.id) {
  throw new Error(`id drift: entrypoint='${extension.id}' manifest='${manifest.id}'`)
}
await host.startExtension(extension, { manifest })

Prevention

When it happens

Trigger: Loading a manifest.json whose id field was edited (e.g. renamed package) while the entrypoint still exports the old id; copying an existing extension as a template and changing only one of the two ids; build tooling that rewrites manifest ids (scoped names, versioned ids) without regenerating the entrypoint; publishing under a new name but keeping the source constant.

Common situations: Forking/templating extensions; monorepo codegen that mangles ids; manifest produced by a bundler with different normalization (slashes, @scope) than the runtime string; version bumps that accidentally touch the id.

Related errors


AI-assisted analysis of moeru-ai/airi@677329427f (2026-08-18). Data as JSON: /api/errors/399feea9f0949b4f. Report an issue: GitHub.

Appendix: source

Thrown at packages/plugin-sdk/src/plugin-host/core.ts:240

  constructor(options: ExtensionHostOptions = {}) {
    this.loader = new FileSystemLoader()
    this.runtime = options.runtime ?? 'electron'
    this.permissionResolver = options.permissionResolver
    this.resources.setValue(protocolListProvidersEventName, [] as Array<{ name: string }>)
    this.markCapabilityReady(protocolListProvidersEventName, { source: 'plugin-host' })
    this.installContext = this.createInstallContext()

    for (const contribution of options.contributions ?? []) {
      this.installContribution(contribution)
    }
  }

  async startExtension(
    extension: Extension,
    options: { manifest: ExtensionManifestV1, cwd?: string, runtime?: PluginRuntime },
  ) {
    if (extension.id !== options.manifest.id) {
      throw new Error(`Extension entrypoint id \`${extension.id}\` must match manifest id \`${options.manifest.id}\`.`)
    }

    const sessionIdentity = this.extensionSessionService.nextSessionIdentity()
    const extensionIdentity = {
      id: extension.id,
      version: extension.version,
      sessionId: sessionIdentity.sessionId,
    }
    const persistedGrant = this.persistedPermissionGrants.get(extension.id)
    const resolvedGrant = await this.permissionResolver?.({
      identity: extensionIdentity,
      manifest: options.manifest,
      requested: options.manifest.permissions,
      persisted: persistedGrant,
    }) ?? options.manifest.permissions
    const permissionSnapshot = this.permissions.initialize(sessionIdentity.sessionId, options.manifest.permissions, {
      grant: resolvedGrant,
      persisted: this.permissionResolver ? undefined : persistedGrant,

View on GitHub (pinned to 677329427f)