.agents/skills/plugin-contract-guard/SKILL.md
The "plugin system" spans four surfaces that must stay consistent:
| Surface | Location |
|---|---|
| Manifests & registry | crates/targets/src/{manifest,plugin}.rs |
| Install/enable planning (control plane) | crates/targets/src/control_plane.rs |
| Extension schemas | crates/extension-schema/src/lib.rs, crates/targets/src/catalog/extension.rs |
| Admin API contract | rustfs/src/admin/plugin_contract.rs, handlers/{plugins_catalog,plugins_instances,extensions,target_descriptor}.rs |
Secrets have one source of truth. Secret config keys are declared only
in the plugin manifest (TargetPluginManifest.secret_fields,
crates/targets/src/manifest.rs) and flow to admin via
AdminTargetSpec.secret_fields. Never add a hand-maintained per-service
secret table in a handler; if redaction misses a field, fix the manifest.
Redaction must round-trip. Instance GET responses replace secret values
with ***redacted*** (REDACTED_SECRET_VALUE in plugins_instances.rs).
Instance PUT restores the stored secret when it receives that placeholder
back (restore_redacted_secret_values). Any new read or write path for
target config must keep both halves: redact on the way out, restore the
placeholder on the way in. The placeholder literal must never be persisted.
Fixtures never reach production responses.
example_external_webhook_plugin() (crates/targets/src/catalog/mod.rs)
is a test/demo fixture for control-plane planning tests. Production
catalog/extension handlers must not include it; regression tests
(plugin_catalog_never_exposes_example_or_external_fixtures,
extension_catalog_never_exposes_example_or_external_fixtures) enforce it.
External plugin flow is planning-only and deny-by-default.
plan_external_target_plugin_action returns decisions, it executes
nothing. TargetPluginExternalFlowGate::default() is fully closed and
TargetPluginInstallPolicy::default().allowed_download_hosts is empty —
keep it that way; tests opt in via explicit policies. Install validation
requires https, an allowlisted host, a full 64-hex-char sha256 digest,
signature and provenance URIs, and an artifact matching the host
target_triple.
Custom target types must not collide. Unknown target types get an
interned unique custom:<type> plugin id (custom_plugin_id in
manifest.rs). Custom plugins with secrets must register via
TargetPluginDescriptor::with_manifest and declare secret_fields;
::new derives a manifest with no secrets.
plugin_contract.rs tests: insta snapshots
(rustfs/src/admin/snapshots/) plus literal json! assertions. Update
both deliberately; a shape change is a console-facing API change.snake_case, except discovery blocks
(runtimeCapabilities, clusterSnapshot, extensionsCatalog) which are
camelCase by cross-endpoint convention (same shape in system.rs,
console.rs, pools.rs). Do not "fix" that inconsistency locally.rustfs_targets types
(anti-corruption layer). Add a From impl; do not serialize internal
types directly.call and an include_str! guard test asserting it (repo-wide pattern —
see plugin_instance_handlers_require_admin_authorization_contract).GetBucketTargetAction (instances) or ServerInfoAdminAction
(catalogs); writes use SetBucketTargetAction.refresh_persisted_module_switches), then evaluate the sync
module_disabled_block_reason per domain — do not re-read the store per
domain or per instance.Event-payload generics use the PluginEvent blanket trait
(crates/targets/src/plugin.rs). Do not respell
Send + Sync + 'static + Clone + Serialize + DeserializeOwned.