How to package, distribute, version, and migrate plugins. The machinery: PluginRegistry (plugin/registry.go), config export/import (plugin/export.go), storage migrations (plugin/migration.go), and checksums (plugin/manifest.go VerifyChecksum).
my-plugin/
├── plugin.json # manifest — the entry point for discovery
├── main.go # or main.sh / main.py / bin/
└── web/ # optional frontend module (index.js + chunks)
Install = copy the directory under ~/.xbot/plugins/<id>/ (or an extra search dir registered via PluginManager.AddSearchDirs). That’s the entire distribution story for the MVP — there is no archive format yet.
PluginRegistry (plugin/registry.go) wraps a PluginManager with sources (RegistrySource{Type, URL}) where Type is github/url/local:
Search(ctx, query)— case-insensitive match against ID/Name/Description of locally installed plugins only.Install(ctx, id)— MVP supports local sources only:URLis the plugin directory path;InstallPlugincopies it. GitHub/URL sources are defined butInstallFromSourceis Phase 3.RegistryEntrycarries ID/Name/Version/Description/Author/Source/DownloadURL/Checksum (SHA256 of the plugin archive).
VerifyChecksum(dir) (plugin/manifest.go) verifies the SHA256 recorded in the manifest against directory contents. Use it to detect tampering or incomplete copies:
if err := plugin.VerifyChecksum(pluginDir); err != nil {
// reject the plugin — contents don't match the declared checksum
}
- ID: regex
^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}$(isValidPluginID,plugin/manifest.go) — reverse DNS recommended (com.example.echo-channel,xbot.git-fancy). This same regex guards the static-serving path (isValidPluginIDForServe). - Version: strict semver, parsed by
ParseVersion/parseSemver. Invalid versions fail manifest loading. - Manifest
homepage+authorfields exist for marketplace display.
PluginManager emits PluginEventInstalled/PluginEventUninstalled/PluginEventReloaded/PluginEventError via PluginEventNotifier (plugin/events.go). Consumers subscribe with pm.OnPluginEvent(cb) — a marketplace UI or audit system listens here. All actions land in the audit log (~/.xbot/plugins/audit.jsonl).
InstallPlugin uses filepath.EvalSymlinks to resolve the real path before deletion checks — only directories under xbotHome are ever deleted (symlink traversal protection).
PluginManager.ExportConfig() serializes all plugins’ manifests, states, and user configs; ImportConfig(data) restores configs for locally-present plugins (missing ones skipped) and merges the disabled set (union):
data, _ := pm.ExportConfig() // ConfigExport{Version, ExportedAt, Disabled, Plugins}
err := pm.ImportConfig(data) // best-effort restore
⚠️ ExportConfig acquires the manager RLock — never call it from inside a plugin’s Activate/Deactivate (which run under the write lock).
Plugins that persist state must migrate it across versions. plugin/migration.go:
plugin.RegisterMigration("xbot.demo", plugin.PluginMigration{
FromVersion: "1.0.0",
ToVersion: "1.1.0",
Migrate: func(ctx context.Context, storage plugin.StorageAccessor) error {
old, _ := storage.Get("count")
storage.Set("counters.total", old) // restructure the schema
return nil
},
})
RunMigrations(pluginID, storage)orders steps by semver and applies pending ones exactly once (recorded under_migrations).- The
Migratortakes a backup before each run:~/.xbot/plugins/<id>/backups/<version>/. Rollback restores the latest backup. - Migrations run sequentially in version order.
plugin.jsonparses (python3 -m json.tool) and version bumps are semver.- Permissions are minimal and complete (see Permissions).
go build ./.../go test ./...for Go backends;vitestfor web views.- Manifest permission test (git-fancy
TestManifestPermissionspattern) passes. - Storage migrations registered for schema changes; backup/restore verified manually.
VerifyChecksumpasses on the release directory.- README documents the plugin ID, runtime, permissions, and configuration schema.
- For channel plugins: users must also add
channels.<name>.enabled=trueto config.json — document it prominently. - Ship the built frontend bundle (for web plugins) under
web/; never commit dev-only files there.