core/abi/proto/v1/manifest.proto
Alex Dunmow 04991295cf feat(abi,blocks): ninjatpl rendering as a host capability (powered blocks + tag/filter callbacks)
pongo2 stays host-side and is never compiled into a guest; plugins render
templates by handing the host a {template, data} pair, and the host calls
back into the free guest instance for plugin-declared tags/filters.

ABI (additive, buf-breaking clean):
- RenderBlockResponse gains a `powered` PoweredBlock{template, data_json};
  a block returns EITHER html OR powered.
- New hooks HOOK_RENDER_TAG (10) / HOOK_APPLY_FILTER (11) with
  RenderTag{Request,Response} and ApplyFilter{Request,Response}.
- PluginManifest gains repeated declared_tags / declared_filters (31/32).

Guest SDK (core/blocks, core/plugin/wasmguest):
- blocks.PoweredBlock(template, data) / DecodePoweredBlock: NUL-sentinel
  marker so BlockFunc's string signature is unchanged (smallest additive
  change — no ripple to existing blocks or the host guest-side).
- blocks.RegisterTag / RegisterFilter (+ RenderContext = context.Context)
  write a package-level registry; runRegister resets it per registration
  for deterministic DESCRIBE + dispatch.
- DESCRIBE emits declared_tags/filters; dispatch handles RENDER_TAG /
  APPLY_FILTER (fn errors → response.error; panics → AbiError INTERNAL,
  instance stays callable).

Docs: core/docs/wasm-abi.md gains the render-as-a-host-capability model,
the powered-block flow (re-entrancy-free), the plugin API, and the
HOST-SIDE CONTRACT the cms phase implements.

Tests: unit round-trips for powered/RENDER_TAG/APPLY_FILTER (dispatch +
error + unknown + panic + per-guest registry isolation) plus a real
wazero round-trip through the compiled fixture module. Verified no
guest-reachable package imports pongo2 (go list -deps on the wasip1
fixture build is clean).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 02:36:39 +08:00

232 lines
8.0 KiB
Protocol Buffer

// manifest.proto — the static plugin manifest (WO-WZ-001).
//
// PluginManifest is the wire form of everything that is *static data* in
// core/plugin/registration.go (PluginRegistration). It is produced once at
// publish time by calling the guest's DESCRIBE hook and stored as manifest.pb
// inside the .bnp artifact; the CMS loader reads it without instantiating the
// wasm module.
//
// Function-valued registration fields cannot cross the wire; they map to
// either a hook (Load/Unload/JobHandlers/MediaHooks/HTTPHandler), a boolean
// presence flag here, or an artifact directory (Assets → assets/,
// Schemas → schemas/, Migrations → migrations/). See core/docs/wasm-abi.md
// for the full field-by-field mapping.
syntax = "proto3";
package abi.v1;
option go_package = "git.dev.alexdunmow.com/block/core/abi/v1;abiv1";
// PluginManifest mirrors the static surface of plugin.PluginRegistration.
message PluginManifest {
// ABI major version the plugin was built against. The host rejects
// manifests whose major version it does not support.
uint32 abi_version = 1;
// PluginRegistration.Name / .Version.
string name = 2;
string version = 3;
// PluginRegistration.Dependencies.
repeated Dependency dependencies = 4;
// Blocks registered via BlockRegistry.Register (captured by DESCRIBE).
repeated BlockMeta blocks = 5;
// Blocks registered via BlockRegistry.RegisterTemplateOverride[WithSource].
repeated BlockTemplateOverride block_template_overrides = 6;
// Templates registered via TemplateRegistry (captured by DESCRIBE).
repeated string template_keys = 7; // TemplateRegistry.Register
repeated SystemTemplateMeta system_templates = 8; // RegisterSystemTemplate
repeated PageTemplateMeta page_templates = 9; // RegisterPageTemplate
repeated string email_wrapper_system_keys = 10; // RegisterEmailWrapper
// PluginRegistration.AdminPages.
repeated AdminPage admin_pages = 11;
// PluginRegistration.SettingsSchema / .ThemePresets / .BundledFonts (JSON).
bytes settings_schema = 12;
bytes theme_presets = 13;
bytes bundled_fonts = 14;
// PluginRegistration.MasterPages.
repeated MasterPageDefinition master_pages = 15;
// PluginRegistration.AIActions.
repeated AiAction ai_actions = 16;
// RBAC roles for the plugin's own Connect services, merged from
// ServiceRegistration (full method name → role, e.g.
// "/symposium.v1.ForumService/CreateThread" → "admin").
map<string, string> rbac_method_roles = 17;
// PluginRegistration.CSSManifest.
CssManifest css_manifest = 18;
// PluginRegistration.RequiredIconPacks.
repeated string required_icon_packs = 19;
// PluginRegistration.DirectoryExtensions (static fields only; the
// panel-section / pin-decorator callbacks are counted so the host knows
// how many guest callbacks exist).
DirectoryExtensions directory_extensions = 20;
// Job types the plugin handles (keys of PluginRegistration.JobHandlers).
// The host dispatches these via the JOB hook.
repeated string job_types = 21;
// Content types the plugin registered RAG content fetchers for
// (RAGService.RegisterContentFetcher inverts to a manifest declaration;
// the host calls back via the RAG_FETCH hook).
repeated string rag_content_fetcher_types = 22;
// PluginRegistration.SettingsPanel — Module Federation path of the
// settings panel component ("" when the plugin has none).
string settings_panel = 23;
// Presence flags for function-valued registration fields that invert to
// hooks at runtime.
bool has_http_handler = 24; // PluginRegistration.HTTPHandler → HANDLE_HTTP
bool has_load_hook = 25; // PluginRegistration.Load → LOAD
bool has_unload_hook = 26; // PluginRegistration.Unload → UNLOAD
bool has_media_hooks = 27; // PluginRegistration.MediaHooks → MEDIA_HOOK
bool has_provisioner = 28; // PluginRegistration.RegisterWithProvisioner
// Core CMS services the plugin mounts with custom RBAC roles
// (CoreServiceBindings.Bind becomes a static declaration; the host
// constructs and mounts the handlers).
repeated CoreServiceBinding core_service_bindings = 29;
// data_dir requests a persistent per-plugin /data preopen at load. Its
// source of truth is the plugin.mod `data_dir = true` key, which lives
// outside the guest code, so DESCRIBE cannot populate it: the packer
// (`ninja plugin build`) stamps it into the manifest from plugin.mod so
// the loader reads one source. OFF by default.
bool data_dir = 30;
// Template tags the plugin provides (blocks.RegisterTag). For each name the
// host registers a pongo2 tag that invokes the guest via HOOK_RENDER_TAG.
// Captured by DESCRIBE from the blocks tag registry (Register-time).
repeated string declared_tags = 31;
// Template filters the plugin provides (blocks.RegisterFilter). For each
// name the host registers a pongo2 filter that invokes the guest via
// HOOK_APPLY_FILTER. Captured by DESCRIBE from the blocks filter registry.
repeated string declared_filters = 32;
}
// Dependency mirrors plugin.Dependency.
message Dependency {
string plugin = 1;
string min_version = 2;
bool required = 3;
}
// BlockMeta mirrors blocks.BlockMeta. Source is omitted: the host assigns it
// from the manifest's plugin name at load time.
message BlockMeta {
string key = 1;
string title = 2;
string description = 3;
// blocks.BlockCategory string ("content", "layout", "navigation", "blog",
// "theme").
string category = 4;
bool has_internal_slot = 5;
bool hidden = 6;
string editor_js = 7;
}
// BlockTemplateOverride captures BlockRegistry.RegisterTemplateOverride and
// RegisterTemplateOverrideWithSource registrations.
message BlockTemplateOverride {
string template_key = 1;
string block_key = 2;
// Override source label; empty for plain RegisterTemplateOverride.
string source = 3;
}
// SystemTemplateMeta mirrors templates.SystemTemplateMeta.
message SystemTemplateMeta {
string key = 1;
string title = 2;
string description = 3;
}
// PageTemplateMeta mirrors templates.PageTemplateMeta plus the system
// template it was registered under.
message PageTemplateMeta {
string system_key = 1;
string key = 2;
string title = 3;
string description = 4;
repeated string slots = 5;
}
// AdminPage mirrors plugin.AdminPage.
message AdminPage {
string key = 1;
string title = 2;
string icon = 3;
string route = 4;
}
// AiAction mirrors plugin.AIAction.
message AiAction {
string key = 1;
string title = 2;
string description = 3;
string default_provider = 4;
string default_model = 5;
}
// CssManifest mirrors plugin.CSSManifest.
message CssManifest {
map<string, string> npm_packages = 1;
repeated string css_directives = 2;
string input_css_append = 3;
}
// DirectoryExtensions mirrors the static fields of plugin.DirectoryExtensions.
message DirectoryExtensions {
repeated string boolean_filter_fields = 1;
repeated string select_filter_fields = 2;
map<string, BadgeLabel> badge_labels = 3;
// Counts of the callback slices (PanelSections / PinDecorators) so the host
// knows how many guest callbacks the plugin registered. Their invocation
// hook is a runtime-WO concern.
uint32 panel_section_count = 4;
uint32 pin_decorator_count = 5;
}
// BadgeLabel mirrors the [2]string value of DirectoryExtensions.BadgeLabels.
message BadgeLabel {
string positive = 1;
string negative = 2;
}
// MasterPageDefinition mirrors plugin.MasterPageDefinition.
message MasterPageDefinition {
string key = 1;
string title = 2;
repeated string page_templates = 3;
repeated MasterPageBlock blocks = 4;
}
// MasterPageBlock mirrors plugin.MasterPageBlock.
message MasterPageBlock {
string block_key = 1;
string title = 2;
// JSON encoding of the block's content map.
bytes content_json = 3;
optional string html_content = 4;
string slot = 5;
int32 sort_order = 6;
}
// CoreServiceBinding mirrors a plugin.CoreServiceBindings.Bind call: mount
// the named core-provided Connect service with these RBAC roles.
message CoreServiceBinding {
string service_name = 1;
map<string, string> method_roles = 2;
}