diff --git a/docs/adr/0004-plugin-manifests-declare-admin-api-compatibility.md b/docs/adr/0004-plugin-manifests-declare-admin-api-compatibility.md new file mode 100644 index 0000000..1c069fd --- /dev/null +++ b/docs/adr/0004-plugin-manifests-declare-admin-api-compatibility.md @@ -0,0 +1,37 @@ +# Plugin manifests declare admin API compatibility + +Plugin admin bundles consume the versioned `@block-ninja/api` browser package. +The existing `block_core` compatibility constraint describes the host plugin +runtime, but it cannot tell a registry or CMS whether an admin bundle is safe +to load against the host's browser API. Treating those surfaces as one version +would couple independent release cycles and allow an otherwise compatible +plugin to fail only after its admin page loads. + +Decision: `[compatibility]` gains an optional `admin_api` string. Its value is +a semantic-version constraint for the host-provided browser admin API, for +example `admin_api = ">=0.1.2"`. The manifest/parser layer preserves the value +verbatim; registry and host resolvers own constraint validation and matching. + +An omitted or empty value means the plugin has not declared browser API +compatibility. It is not equivalent to an unconstrained wildcard. Automated +latest-compatible resolution that filters by a host admin API version must +therefore fail closed for an undeclared value. This does not change the +separate `block_core` constraint or imply that every plugin has an admin +bundle. + +The field is part of the shared manifest model and the CLI's hand-written +`plugin.mod` serializer so version bumps, tag edits, and initialization cannot +silently discard it. + +Consequences: + +- Registries and hosts can select plugin releases compatible with both the + runtime ABI and browser admin API. +- Headless plugins can continue to omit `admin_api`. +- Plugin authors express a range rather than pinning a single browser package + version. +- Constraint syntax and matching remain the resolver's responsibility rather + than being duplicated in manifest parsers. + +Keywords: plugin.mod, compatibility, admin_api, AdminAPI, @block-ninja/api, +semantic version, browser API, fail closed, plugin resolver diff --git a/plugin/mod.go b/plugin/mod.go index 07e9fa5..515edf3 100644 --- a/plugin/mod.go +++ b/plugin/mod.go @@ -91,6 +91,7 @@ type ModPublicRoute struct { type ModCompat struct { BlockCore string `toml:"block_core"` + AdminAPI string `toml:"admin_api"` } type ModRequirement struct { diff --git a/plugin/mod_test.go b/plugin/mod_test.go index 7eaa9ce..ab5ab83 100644 --- a/plugin/mod_test.go +++ b/plugin/mod_test.go @@ -278,6 +278,7 @@ version = "0.2.0" [compatibility] block_core = ">=1.5 <2.0" +admin_api = ">=0.1.2 <0.2.0" [[requires]] name = "@blockninja/smartblock" @@ -291,8 +292,14 @@ version = ">=1.2" if err != nil { t.Fatalf("ParseModFull err: %v", err) } - if m.Compatibility == nil || m.Compatibility.BlockCore != ">=1.5 <2.0" { - t.Errorf("Compat = %+v", m.Compatibility) + if m.Compatibility == nil { + t.Fatal("Compatibility is nil") + } + if m.Compatibility.BlockCore != ">=1.5 <2.0" { + t.Errorf("Compat.BlockCore = %q", m.Compatibility.BlockCore) + } + if m.Compatibility.AdminAPI != ">=0.1.2 <0.2.0" { + t.Errorf("Compat.AdminAPI = %q", m.Compatibility.AdminAPI) } if len(m.Requires) != 2 { t.Fatalf("Requires len = %d, want 2", len(m.Requires))