{"record":{"id":"159f4c87d58308b6","repo":"moeru-ai/airi","slug":"airi-version-must-be-a-valid-semantic-version-options","errorCode":null,"errorMessage":"AIRI version must be a valid semantic version: ${options.airiVersion}","messagePattern":"AIRI version must be a valid semantic version: (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/plugin-sdk/src/plugin-host/core.ts","lineNumber":229,"sourceCode":"  private readonly dependencies = new DependencyService()\n  private readonly kits = new KitRegistryService()\n  private readonly kitApis = new Map<string, KitRef<unknown>>()\n  private readonly kitApiWatchers = new Map<string, Set<() => Promise<void>>>()\n  private readonly modules = new KitApiBindingRegistryService()\n  private readonly extensionModuleResources = new Map<string, ExtensionModuleResourceTracker>()\n  private readonly permissions = new PermissionService()\n  private readonly airiVersion?: string\n  private readonly permissionResolver?: ExtensionHostOptions['permissionResolver']\n  private readonly persistedPermissionGrants = new Map<string, ModulePermissionGrant>()\n  private readonly resources = new ResourceService()\n\n  private readonly installContext: ExtensionHostInstallContext\n\n  constructor(options: ExtensionHostOptions = {}) {\n    this.loader = new FileSystemLoader()\n    this.runtime = options.runtime ?? 'electron'\n    if (options.airiVersion && !semver.valid(options.airiVersion)) {\n      throw new Error(`AIRI version must be a valid semantic version: ${options.airiVersion}`)\n    }\n    this.airiVersion = options.airiVersion\n    this.permissionResolver = options.permissionResolver\n    this.resources.setValue(protocolListProvidersEventName, [] as Array<{ name: string }>)\n    this.markCapabilityReady(protocolListProvidersEventName, { source: 'plugin-host' })\n    this.installContext = this.createInstallContext()\n\n    for (const contribution of options.contributions ?? []) {\n      this.installContribution(contribution)\n    }\n  }\n\n  private assertManifestCompatibility(manifest: ExtensionManifestV2, runtime: PluginRuntime) {\n    if (!manifest.engines.runtimes.includes(runtime)) {\n      throw new Error(`Extension \\`${manifest.id}\\` does not support runtime \\`${runtime}\\`.`)\n    }\n    if (this.airiVersion && !semver.satisfies(this.airiVersion, manifest.engines.airi, { includePrerelease: true })) {\n      throw new Error(","sourceCodeStart":211,"sourceCodeEnd":247,"githubUrl":"https://github.com/moeru-ai/airi/blob/438a067dde47aa0bdb46c2323d1fe293dc805218/packages/plugin-sdk/src/plugin-host/core.ts#L211-L247","documentation":"The ExtensionHost constructor validates options.airiVersion with semver.valid before storing it, because the version is later checked against extension manifests' engines.airi ranges via semver.satisfies. If airiVersion is provided but is not a valid semantic version string, the constructor throws at startup rather than failing later with confusing extension-compatibility errors.","triggerScenarios":"new ExtensionHost({ airiVersion: '...' }) is called where airiVersion is a truthy string that fails semver.valid — e.g. 'v1.2' (missing patch), '1.2.3.4', 'beta', an app.getPath/getVersion output in a non-semver format, or an env var string with whitespace.","commonSituations":"Wiring the host to app.getVersion() in an Electron build whose version was overridden; reading AIRI version from an env var or config file with a loose value; passing a semver range like '^1.0.0' instead of a concrete version; forgetting the leading 'v' is fine for semver.valid but extra suffixes are not.","solutions":["Pass a valid full semver string (MAJOR.MINOR.PATCH, optional prerelease/build), e.g. '1.2.3'.","Normalize the source value first: strip leading 'v'/'=', trim whitespace, and coerce with semver.coerce(value) when the raw string may contain extra text.","If no reliable version is known, omit airiVersion entirely — it is optional and skips the compatibility check.","Log/validate the value at the call site so a bad build/config value is caught before host construction."],"exampleFix":"// before\nnew ExtensionHost({ airiVersion: remoteConfig.version })\n\n// after\nimport semver from 'semver'\nconst coerced = semver.coerce(remoteConfig.version)\nnew ExtensionHost({ airiVersion: coerced ?? undefined })","handlingStrategy":"validation","validationCode":"import semver from 'semver'\nconst v = options.airiVersion\nif (v != null && !semver.valid(v)) throw new Error(`refusing to start: airiVersion '${v}' is not valid semver`)","typeGuard":"function isValidAiriVersion(v) {\n  return v == null || (typeof v === 'string' && semver.valid(v) !== null)\n}","tryCatchPattern":"let host\ntry {\n  host = new ExtensionHost({ airiVersion })\n} catch (err) {\n  if (String(err.message).startsWith('AIRI version must be a valid semantic version')) {\n    console.error(`Bad airiVersion '${airiVersion}'; falling back to no version pin.`, err)\n    host = new ExtensionHost()\n  } else throw err\n}","preventionTips":["Always source airiVersion from a semver-conformant source (app.getVersion() of a properly tagged build), never free-form config.","Coerce with semver.coerce() and trim the string before constructing the host.","Omit airiVersion rather than passing a placeholder like 'dev' or 'unknown'.","Validate the value in CI or at bootstrap so misconfigured builds fail early and visibly."],"tags":["semver","constructor","plugin-host","config"],"backgroundTag":"invalid-argument-format","analyzedSha":"438a067dde47aa0bdb46c2323d1fe293dc805218","analyzedAt":"2026-09-17T01:14:42.644Z","contentChangedAt":"2026-09-17T01:14:42.644Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}