Skip to content

Latest commit

 

History

History
70 lines (51 loc) · 12.4 KB

File metadata and controls

70 lines (51 loc) · 12.4 KB

Plugin API — surface, stability & compatibility

Guidance for maintainers changing the Code on the Go plugin API. It defines what counts as the plugin contract, the current compatibility policy, and how to change the API without breaking plugins already in the field by accident.

This is not a plugin-authoring how-to. For that — project layout, manifest contract, building a .cgp — see PLUGIN_AUTHORING.md (the in-repo author guide), the Plugin Development wiki (https://appdevforall.atlassian.net/wiki/x/BQANIQ, the authoritative API reference), and the in-tree example plugins.

What counts as the plugin API (the contract)

The surface a plugin binds to is broader than one module. All of the following are contract:

  • The :plugin-api module — package com.itsaky.androidide.plugins.*:
    • Core: IPlugin (lifecycle), PluginContext, PluginLogger, ServiceRegistry, ResourceManager.
    • Extension interfaces plugins implement: UIExtension, EditorExtension, EditorTabExtension, DocumentationExtension, BuildActionExtension, SnippetExtension, ProjectExtension, FileOpenExtension, SettingsExtension, LanguageExtension.
    • IDE service interfaces plugins call (via ServiceRegistry.get(X::class.java)): IdeProjectService, IdeEditorService, IdeFileService, IdeEnvironmentService, IdeArchiveService, IdeBuildService, IdeLogService, IdeUIService, IdeEditorTabService, IdeTooltipService, IdeThemeService, IdeFeatureFlagService, IdeCommandService, IdeTemplateService, IdeSnippetService, IdeSidebarService.
    • Cross-plugin service interfaces, where one plugin implements what another calls (via SharedServices): LlmInferenceService — implemented by ai-core, called by every AI plugin — together with the types nested in it that a backend plugin implements (LlmBackend, HistoryCapableBackend, ToolCallingBackend, CancellableBackend, ConfigurableBackend, EmbeddingBackend) and the value types either side constructs (ChatMessage, LlmConfig, LlmResponse, SystemPromptRequest, ToolDefinition, ToolCallRequest). Also ToolSourceRegistry — implemented by ai-core, called by any plugin contributing tools to the agent — with ToolSource and ToolSpec, which a contributing plugin implements, ToolInvocation, which ai-core constructs and passes to ToolSource.invoke, and ToolOutcome, which the source returns.
    • Utility classes plugins instantiate: KeystoreSecretStore (AES/GCM over the Android Keystore, alias supplied by the caller); the AI prompt config engine in ai.prompt (PromptTemplateEngine, PromptConfigLoader, PromptConfigStore, PromptConfigDocument, PromptConfigObject, AssetPromptConfigSource), generic over a plugin's own config type through PromptConfigParser and PromptConfigProvider, which a plugin implements; and the settings-pane helpers in ai.ui (SecretRevealController with RevealToggle, applyPaneStyling with PaneStyle, ButtonColors and FieldColors). Host-side implementations rather than interfaces, so plugins share one copy in the process instead of compiling their own.
    • Data classes plugins construct (e.g. MenuItem, TabItem, EditorTabItem, NavigationItem, ToolbarAction, FabAction, PluginBuildAction, SnippetContribution, PluginTooltipEntry, PluginSettingsEntry, LanguageDefinition, TreeSitterGrammar, LanguageServerDefinition).
    • Cross-plugin service interfaces, where one plugin implements what another calls (via SharedServices): LlmInferenceService — implemented by AI-Core, called by every AI plugin — together with the types nested in it that a backend plugin implements (LlmBackend, HistoryCapableBackend, ToolCallingBackend, CancellableBackend, ConfigurableBackend, EmbeddingBackend, EmbeddingModelSelectable, StatusReportingBackend, ActiveModelReportingBackend), BackendChangeListener, which a consumer implements, and the value types either side constructs (ChatMessage, LlmConfig, LlmResponse, SystemPromptRequest, ToolDefinition, ToolCallRequest). Also ToolSourceRegistry — implemented by AI-Core, called by any plugin contributing tools to the agent — with ToolSource, StatusReportingToolSource, GroupedToolSource, ToolSpec and ToolGroup, which a contributing plugin implements, ToolSourceListener, which a consumer implements, ToolInvocation, which AI-Core constructs and passes to ToolSource.invoke, and ToolOutcome, which the source returns. Both contracts report health as the top-level CapabilityStatus enum.
    • Utility classes plugins instantiate: KeystoreSecretStore (AES/GCM over the Android Keystore, alias supplied by the caller); the AI prompt config engine in ai.prompt (PromptTemplateEngine, PromptConfigLoader, PromptConfigStore, PromptConfigDocument, PromptConfigObject, AssetPromptConfigSource), generic over a plugin's own config type through PromptConfigParser and PromptConfigProvider, which a plugin implements; and the settings-pane helpers in ai.ui (SecretRevealController with RevealToggle, applyPaneStyling with PaneStyle, ButtonColors and FieldColors); and ai.LlmBackendRegistration, which keeps a backend plugin registered with AI-Core's router. Host-side implementations rather than interfaces, so plugins share one copy in the process instead of compiling their own.
    • Data classes plugins construct (e.g. MenuItem, TabItem, EditorTabItem, NavigationItem, ToolbarAction, FabAction, PluginBuildAction, SnippetContribution, PluginTooltipEntry, PluginSettingsEntry).
    • Enums / sealed types plugins reference: PluginPermission, ShowAsAction, ArchiveFormat, BuildActionCategory, ToolbarActionIds, CommandSpec, CommandResult, ExtractResult, KeystoreSecretStore.Stored. Sealed, so a plugin whens over the cases exhaustively — adding one is a breaking change, not an additive one.
  • Wire/format contracts outside the module:
    • Manifest <meta-data> keys — plugin.id, plugin.name, plugin.version, plugin.description, plugin.author, plugin.main_class, plugin.min_ide_version, plugin.max_ide_version, plugin.permissions, plugin.sidebar_items, plugin.icon_day, plugin.icon_night, plugin.vcs_revision, plugin.build_timestamp. Matched by string — a rename silently breaks every plugin.
    • Plugin language assets: lib/<abi>/libtree-sitter-<name>.so exporting tree_sitter_<name>, the query file names under TreeSitterGrammar.queriesAssetPath, and the standard capture names mapped by each colour scheme's generic.json.
    • Permission key strings (filesystem.read, filesystem.write, network.access, system.commands, ide.settings, project.structure, native.code, ide.environment.write) — also matched by string.
    • The path allowlist and per-plugin data directories enforced by IdeFileService / IdeArchiveService.
    • File/format contracts: the .cgp package format (including assets/cgp-build.properties, the provenance record the builder writes into every artifact — see PLUGIN_AUTHORING.md), the .cgt template format, the plugin_documentation.db schema, .codeonthego/scripts.json, the TextMate snippet syntax, and the http://localhost:6174/ help-server URL scheme with the plugin/<pluginId>/ namespace.

Current compatibility policy — read this

The plugin API is still evolving and is NOT frozen. We do not yet guarantee source or binary backward compatibility across IDE releases. This is a deliberate early-stage decision — we're still discovering what plugins need, and freezing too early would lock in mistakes.

That is not license to break plugins casually. The rule for now:

Every change to the surface above must be deliberate, documented, and justified. Know when you're changing the contract, say so in the PR (what breaks, who's affected, why it's worth it), and record it here or in a changelog. Prefer additive changes; gate behavior with plugin.min_ide_version / plugin.max_ide_version where it helps.

When the API is later frozen, this doc gains a formal compatibility guarantee and (ideally) automated enforcement — see Follow-ups.

Know when you're making a breaking change (Kotlin traps)

These look source-compatible but break already-built .cgp plugins:

  • Data-class constructor parameters. Adding a parameter even with a default value changes the synthetic constructor and copy() signatures — binary-incompatible for any plugin that constructs or copies the class (MenuItem, PluginBuildAction, SnippetContribution, …). If compatibility matters, add a secondary constructor or a builder instead.
  • Interface methods — direction matters. Ask who implements the interface before you apply a rule; the answer is not "host" just because the name ends in Service.
    • Extension interfaces (UIExtension, BuildActionExtension, …) are implemented by plugins: adding a method is breaking for them (even a defaulted one can break depending on compilation). Provide defaults and prefer additive optional hooks.
    • Host service interfaces (Ide*Service) are implemented by the host and only called by plugins: adding a method is safe; changing or removing a signature is breaking.
    • Plugin-implemented service interfaces (LlmInferenceService and the backend interfaces nested in it) are implemented by a plugin even though they are shaped like services. The extension-interface rule applies, not the host-service one: adding a method is breaking. A Kotlin implementor that already declares a same-signature method without override stops compiling when a Java default appears above it, so the break is in the other repo — the impact check below catches it. Prefer a new interface extending the old one over a new method on it: StatusReportingBackend, ActiveModelReportingBackend and StatusReportingToolSource follow this, and the consumer asks with instanceof. The same rule covers ToolSource, which every contributing plugin implements. LlmInferenceService and ToolSourceRegistry themselves took default methods in 26.41 as an exception: AI-Core is their only implementor, and its released implementations were checked to declare none of the new names.
  • Enum constants. Removing or renaming a constant (PluginPermission, ShowAsAction, ArchiveFormat, ToolbarActionIds, BuildActionCategory) breaks plugins that name it; adding one can still break an exhaustive when.
  • Types & nullability. Flipping nullable↔non-null, changing a parameter/return type, or val↔var on an API property.
  • Moving or renaming any class/package under com.itsaky.androidide.plugins.* — breaks imports and ServiceRegistry.get(...) lookups.
  • Manifest key or permission-string renames — silently break every existing plugin, since both are matched by string.

Before you change the plugin API — checklist

  • Is the change to :plugin-api (or a manifest key / permission string / format) actually necessary? Prefer additive over breaking.
  • If it breaks compatibility, is that called out explicitly in the PR — what breaks, who's affected, why it's worth it?
  • Recorded here or in a changelog so plugin authors can find it.
  • Impact-checked against the example plugins under plugins/ in the external addons repo — APK-Analyzer, Markdown-Previewer, Keystore-Generator, and, for significant changes, the rest (e.g. AI-Core): do they still compile and load?
  • Version gating (plugin.min_ide_version / plugin.max_ide_version) considered if runtime behavior changes.

Follow-ups

  • Binary-compatibility checking is in place (since 26.28). :plugin-api now ships a checked-in ABI dump (plugin-api/api/plugin-api.api) enforced by Kotlin's binary-compatibility-validator, so a public-API change shows up as a diff and can gate CI. Members annotated @InternalPluginApi are excluded. Still open: a formal compatibility guarantee (the policy above) once the API firms up.

Related

  • PLUGIN_AUTHORING.md — the author-facing how-to (layout, manifest, icons, building/installing).
  • PLUGIN_API_CHANGELOG.md — which YY.WW release first shipped each capability (for choosing plugin.min_ide_version).
  • REVIEW.md §13 — reviewing a change's impact on plugins.
  • ARCHITECTURE.md — where :plugin-api / :plugin-manager sit in the module map.