pluginsdk/abi/proto/v1/capability.proto
Alex Dunmow b6d40ed8ac feat: bootstrap block/pluginsdk — the proto-first plugin SDK (P1)
Bootstrap the plugin-facing SDK module so the fleet migration (P2) becomes a
mechanical block/core/X -> block/pluginsdk/X import rewrite.

- abi/proto/v1: the single source-of-truth ABI proto tree, copied from the cms
  authoring source (cms/backend/abi/proto/v1) with go_package retargeted to
  git.dev.alexdunmow.com/block/pluginsdk/abi/v1;abiv1. Kills the hand-kept
  cms/core proto duplication (audit gap 4).
- abi/v1: generated Go bindings (buf generate); byte-identical to core's abi/v1
  save the embedded go_package path.
- plugin/ (registration + DI surface), plugin/wasmguest/** (transport shim,
  caps stubs, bnwasm db driver, testdata fixtures + golden .pb), and the
  guest-facing type packages: blocks (+builtin/shared/tags), templates
  (+pongo/bn), auth, settings, content, gating, crypto, rbac, video, ai,
  subscriptions, menus, datasources. Internal imports rewritten core ->
  pluginsdk; zero block/core references remain.
- README/AGENTS(+CLAUDE symlink)/Makefile: proto is the contract, Go is one
  binding; no replace directives; templates/bn is a synced copy authored in cms.

Spec: cms docs/superpowers/specs/2026-07-07-proto-first-plugin-sdk-design.md

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 10:51:00 +08:00

845 lines
20 KiB
Protocol Buffer

// capability.proto — guest→host capability calls (WO-WZ-001).
//
// Each CoreServices interface (core/plugin/deps.go) becomes a host-function
// family; every interface method gets one request/response message pair
// here, mirroring the Go signature 1:1. UUIDs travel as canonical strings;
// map[string]any / []byte-JSON values travel as JSON bytes.
//
// Calls cross via the generic HostCallRequest/HostCallResponse envelope; the
// method string selects the pair (e.g. "content.get_author_profile" →
// ContentGetAuthorProfileRequest/Response). Full method table in
// core/docs/wasm-abi.md.
//
// Not represented here by design:
// - CoreServices.Pool → db.proto (the DB driver messages)
// - CoreServices.Interceptors → host-side only (never crosses)
// - CoreServices.AppURL/MediaPath → LoadRequest.host_config (invoke.proto)
// - CoreServices.CoreServiceBindings → manifest core_service_bindings
// - RAGService.RegisterContentFetcher → manifest rag_content_fetcher_types
// + the RAG_FETCH hook (callback inversion)
// - ai.ToolDefinition.Handler → host→guest tool execution is a runtime-WO
// concern (flagged in core/docs/wasm-abi.md)
syntax = "proto3";
package abi.v1;
import "google/protobuf/timestamp.proto";
import "v1/invoke.proto";
option go_package = "git.dev.alexdunmow.com/block/pluginsdk/abi/v1;abiv1";
// HostCallRequest is the generic guest→host capability envelope.
message HostCallRequest {
// Capability method, "<family>.<method>" (e.g. "crypto.encrypt_secret").
string method = 1;
// Serialized family request message.
bytes payload = 2;
}
// HostCallResponse is the generic host→guest capability return envelope.
message HostCallResponse {
// Serialized family response message; empty when error is set.
bytes payload = 1;
// Set when the capability call failed; the guest SDK surfaces it as a
// normal Go error.
AbiError error = 2;
}
// --- content.* (content.Content) ---
message ContentGetAuthorProfileRequest {
string id = 1; // UUID
}
message ContentGetAuthorProfileResponse {
AuthorProfile author = 1;
}
// AuthorProfile mirrors content.AuthorProfile.
message AuthorProfile {
string id = 1; // UUID
string name = 2;
string slug = 3;
string bio = 4;
string avatar_url = 5;
string website = 6;
map<string, string> social_links = 7;
}
message ContentGetPageRequest {
string slug = 1;
}
message ContentGetPageResponse {
PageInfo page = 1;
}
// PageInfo mirrors content.PageInfo.
message PageInfo {
string id = 1; // UUID
string slug = 2;
string title = 3;
}
message ContentGetPostRequest {
string slug = 1;
}
message ContentGetPostResponse {
PostInfo post = 1;
}
// PostInfo mirrors content.PostInfo.
message PostInfo {
string id = 1; // UUID
string slug = 2;
string title = 3;
string excerpt = 4;
string featured_image_url = 5;
string author_id = 6; // UUID
string body = 7; // rendered HTML; set by get_post / list_posts(include_body)
string author_name = 8;
string author_slug = 9;
google.protobuf.Timestamp published_at = 10;
}
message ContentListPostsRequest {
int32 limit = 1;
int32 offset = 2;
string category = 3; // optional category slug filter ("" = all)
bool published_only = 4;
bool include_body = 5;
bool include_excerpt = 6;
}
message ContentListPostsResponse {
repeated PostInfo posts = 1;
}
message ContentSlugifyRequest {
string text = 1;
}
message ContentSlugifyResponse {
string slug = 1;
}
message ContentBlockNoteToHtmlRequest {
// JSON encoding of the BlockNote document map.
bytes doc_json = 1;
}
message ContentBlockNoteToHtmlResponse {
string html = 1;
}
message ContentGenerateExcerptRequest {
string html = 1;
int32 max_len = 2;
}
message ContentGenerateExcerptResponse {
string excerpt = 1;
}
message ContentStripHtmlRequest {
string html = 1;
}
message ContentStripHtmlResponse {
string text = 1;
}
// --- settings.* (settings.Settings) + settings.update (settings.Updater) ---
message SettingsGetSiteSettingsRequest {}
message SettingsGetSiteSettingsResponse {
// JSON encoding of the site settings map.
bytes settings_json = 1;
}
message SettingsGetPluginSettingsRequest {
string plugin_name = 1;
}
message SettingsGetPluginSettingsResponse {
// JSON encoding of the plugin settings map.
bytes settings_json = 1;
}
message SettingsUpdateSiteSettingRequest {
string key = 1;
// JSON encoding of the value.
bytes value_json = 2;
}
message SettingsUpdateSiteSettingResponse {}
// --- gating.* (gating.Gating) ---
message GatingGetSubscriberTierLevelRequest {
string user_id = 1; // UUID
}
message GatingGetSubscriberTierLevelResponse {
int32 level = 1;
}
message GatingEvaluateAccessRequest {
int32 user_tier_level = 1;
// Absent rule means "no rule" (access granted).
AccessRule rule = 2;
}
message GatingEvaluateAccessResponse {
AccessResult result = 1;
}
// AccessRule mirrors gating.AccessRule.
message AccessRule {
int32 min_tier_level = 1;
string override_tier_id = 2;
string teaser_mode = 3; // "hard", "soft", "none"
int32 teaser_percent = 4;
}
// AccessResult mirrors gating.AccessResult.
message AccessResult {
bool has_access = 1;
string teaser_mode = 2;
int32 teaser_percent = 3;
int32 required_level = 4;
}
// --- crypto.* (crypto.Crypto) ---
message CryptoEncryptSecretRequest {
string plaintext = 1;
}
message CryptoEncryptSecretResponse {
string ciphertext = 1;
}
message CryptoDecryptSecretRequest {
string ciphertext = 1;
}
message CryptoDecryptSecretResponse {
string plaintext = 1;
}
// --- menus.* (menus.Menus) ---
message MenusGetMenuByNameRequest {
string name = 1;
}
message MenusGetMenuByNameResponse {
Menu menu = 1;
}
// Menu mirrors menus.Menu.
message Menu {
string id = 1; // UUID
string name = 2;
}
message MenusGetMenuItemsRequest {
string menu_id = 1; // UUID
}
message MenusGetMenuItemsResponse {
repeated MenuItem items = 1;
}
// MenuItem mirrors menus.MenuItem.
message MenuItem {
string id = 1; // UUID
string menu_id = 2; // UUID
string label = 3;
string url = 4;
string page_slug = 5;
optional string parent_id = 6; // UUID; unset mirrors nil *uuid.UUID
int32 sort_order = 7;
bool open_in_new_tab = 8;
string css_class = 9;
string item_type = 10;
string icon = 11;
}
// --- datasources.* (datasources.Datasources) ---
message DatasourcesResolveBucketRequest {
string bucket_id = 1; // UUID
}
message DatasourcesResolveBucketResponse {
DatasourceResult result = 1;
}
message DatasourcesResolveBucketByKeyRequest {
string bucket_key = 1;
}
message DatasourcesResolveBucketByKeyResponse {
DatasourceResult result = 1;
}
// DatasourceResult mirrors datasources.Result ([]any / map[string]any as
// JSON bytes).
message DatasourceResult {
// JSON array of items.
bytes items_json = 1;
int32 total = 2;
// JSON object; empty when absent.
bytes meta_json = 3;
}
// --- users.* (auth.PublicUsers) ---
message UsersGetByUsernameRequest {
string username = 1;
}
message UsersGetByUsernameResponse {
PublicUserProfile user = 1;
}
message UsersGetByIdRequest {
string id = 1; // UUID
}
message UsersGetByIdResponse {
PublicUserProfile user = 1;
}
// PublicUserProfile mirrors auth.PublicUserProfile.
message PublicUserProfile {
string id = 1; // UUID
string email = 2;
string username = 3;
string display_name = 4;
string avatar_url = 5;
string bio = 6;
bool email_verified = 7;
string role = 8;
}
// --- subscriptions.* (subscriptions.Subscriptions) ---
message SubscriptionsGetUserTierLevelRequest {
string user_id = 1; // UUID
}
message SubscriptionsGetUserTierLevelResponse {
TierLevel tier_level = 1;
}
// TierLevel mirrors subscriptions.TierLevel.
message TierLevel {
int32 level = 1;
// JSON feature payload.
bytes features = 2;
}
message SubscriptionsGetTierBySlugRequest {
string slug = 1;
}
message SubscriptionsGetTierBySlugResponse {
Tier tier = 1;
}
// Tier mirrors subscriptions.Tier.
message Tier {
string id = 1; // UUID
string name = 2;
string slug = 3;
int32 level = 4;
string description = 5;
// JSON feature payload.
bytes features = 6;
bool is_default = 7;
int32 position = 8;
}
message SubscriptionsListTiersRequest {}
message SubscriptionsListTiersResponse {
repeated Tier tiers = 1;
}
message SubscriptionsListActivePlansRequest {
string tier_id = 1; // UUID
}
message SubscriptionsListActivePlansResponse {
repeated Plan plans = 1;
}
// Plan mirrors subscriptions.Plan.
message Plan {
string id = 1; // UUID
string tier_id = 2; // UUID
string billing_interval = 3;
int32 amount = 4;
string currency = 5;
bool is_active = 6;
google.protobuf.Timestamp created_at = 7;
}
// --- media.deposit (plugin.Media) ---
// MediaDepositRequest mirrors plugin.MediaDeposit.
message MediaDepositRequest {
// Optional deterministic media row ID (UUID); empty = host generates one.
string id = 1;
string filename = 2;
bytes data = 3;
string alt_text = 4;
string folder = 5;
string source = 6;
}
// MediaDepositResponse mirrors plugin.MediaResult.
message MediaDepositResponse {
string id = 1; // UUID
// Ready-to-use reference ("media:<uuid>").
string ref = 2;
bool created = 3;
}
// --- email.send (plugin.EmailSender) ---
message EmailSendRequest {
string to = 1;
string subject = 2;
string body = 3;
}
message EmailSendResponse {}
// --- ai.text_call (CoreServices.AITextCall) ---
message AiTextCallRequest {
string task_key = 1;
string system_prompt = 2;
string user_message = 3;
}
message AiTextCallResponse {
string text = 1;
}
// --- ai.tools.* (ai.ToolRegistry) ---
// AiToolRegisterRequest mirrors ai.ToolDefinition (minus Handler, which
// stays guest-side; execution direction is a runtime-WO concern).
message AiToolRegisterRequest {
string slug = 1;
string name = 2;
string description = 3;
// JSON encoding of the parameter schema map.
bytes parameter_schema_json = 4;
}
message AiToolRegisterResponse {}
// --- bridge.* (plugin.PluginBridge) ---
//
// The bridge shares in-process Go values today; across sandboxes only the
// registration/lookup surface serializes. Typed cross-plugin calls need a
// runtime design (see core/docs/wasm-abi.md, open items).
message BridgeRegisterServiceRequest {
string plugin_name = 1;
string service_name = 2;
}
message BridgeRegisterServiceResponse {}
message BridgeGetServiceRequest {
string plugin_name = 1;
string service_name = 2;
}
message BridgeGetServiceResponse {
bool available = 1;
}
// --- jobs.submit (plugin.JobRunner) ---
message JobsSubmitRequest {
string job_type = 1;
// JSON job configuration.
bytes config_json = 2;
}
message JobsSubmitResponse {}
// --- embeddings.* (plugin.EmbeddingService) ---
message EmbeddingsGenerateEmbeddingRequest {
string text = 1;
}
message EmbeddingsGenerateEmbeddingResponse {
repeated float embedding = 1;
}
message EmbeddingsEmbedContentRequest {
string source_type = 1;
string source_id = 2; // UUID
string text = 3;
}
message EmbeddingsEmbedContentResponse {
bool embedded = 1;
}
message EmbeddingsIsAvailableRequest {}
message EmbeddingsIsAvailableResponse {
bool available = 1;
}
// --- rag.* (plugin.RAGService) ---
message RagQueryRequest {
string query = 1;
int32 limit = 2;
}
message RagQueryResponse {
repeated RagResult results = 1;
}
// RagResult mirrors plugin.RAGResult.
message RagResult {
string content = 1;
double score = 2;
map<string, string> metadata = 3;
}
message RagOnContentChangedRequest {
string content_type = 1;
string content_id = 2; // UUID
}
message RagOnContentChangedResponse {}
// --- reviews.* (plugin.ReviewSubmitter) ---
// ReviewsSubmitReviewRequest mirrors plugin.SubmitReviewParams.
message ReviewsSubmitReviewRequest {
string table_id = 1; // UUID
string row_id = 2; // UUID
int32 overall_rating = 3;
string review_text = 4;
// JSON encoding of the per-criterion ratings map.
bytes ratings_json = 5;
repeated string photos = 6;
}
message ReviewsSubmitReviewResponse {
string review_id = 1;
}
// --- badges.refresh (plugin.BadgeRefresher) ---
message BadgesRefreshBadgesRequest {
string table_id = 1; // UUID
string row_id = 2; // UUID
}
message BadgesRefreshBadgesResponse {}
// --- content.* writes (content.Author, WO-WZ-019) ---
//
// Imperative page/post authoring. The idempotent seed-time counterparts live
// in the provisioner.* family below; these are for runtime authoring and for
// the WO-WZ-020 codeless seed runner.
message ContentCreatePageRequest {
string slug = 1;
string parent_slug = 2; // "" = root
string title = 3;
string template_key = 4;
string master_page_key = 5; // "" = none
}
message ContentCreatePageResponse {
string page_id = 1; // UUID
// False when a page with this slug already existed (the existing ID is
// returned and nothing is modified).
bool created = 2;
}
// PageBlock is one block instance placed on a page (mirrors
// plugin.PageBlockConfig / MasterPageBlock).
message PageBlock {
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;
}
message ContentSetPageBlocksRequest {
string page_id = 1; // UUID
// Full replacement set for the page's draft document blocks.
repeated PageBlock blocks = 2;
}
message ContentSetPageBlocksResponse {}
message ContentPublishPageRequest {
string page_id = 1; // UUID
}
message ContentPublishPageResponse {}
// ContentSetPageSeoRequest mirrors the CMS UpdatePageSEO surface. Unset
// optionals leave the existing value untouched.
message ContentSetPageSeoRequest {
string page_id = 1; // UUID
optional string meta_title = 2;
optional string meta_description = 3;
optional string og_title = 4;
optional string og_description = 5;
optional string og_image = 6;
optional string focus_keyphrase = 7;
optional string canonical_url = 8;
optional string robots_directive = 9;
optional string twitter_title = 10;
optional string twitter_description = 11;
optional string twitter_image = 12;
}
message ContentSetPageSeoResponse {}
// ContentUpsertPostRequest creates or updates a blog post by slug (posts live
// in the dedicated blog_posts table).
message ContentUpsertPostRequest {
string slug = 1;
string title = 2;
// JSON encoding of the BlockNote document.
bytes document_json = 3;
optional string excerpt = 4;
optional string author_profile_id = 5; // UUID
optional string featured_image_id = 6; // UUID (e.g. from media.deposit)
// Publish immediately after the upsert.
bool publish = 7;
bool is_featured = 8;
}
message ContentUpsertPostResponse {
string post_id = 1; // UUID
bool created = 2;
}
// --- provisioner.* (plugin.Provisioner, WO-WZ-019) ---
//
// Idempotent seed/ensure operations, 1:1 with the plugin.Provisioner Go
// interface. In the wasm world these are LOAD-TIME capabilities: plugins call
// deps.Provisioner from their Load hook (RegisterWithProvisioner cannot cross
// the DESCRIBE boundary — host functions are stubbed there). All methods
// check-then-create; re-running them is safe.
//
// EmbedConfig.RenderFunc deliberately does not cross: an ABI-provisioned
// embed is template-rendered host-side. A compute-rendered embed needs a
// block + hook instead.
message ProvisionerEnsureDataTableRequest {
string key = 1;
string name = 2;
string description = 3;
// JSON schema of the table (json.RawMessage in DataTableConfig).
bytes schema_json = 4;
string primary_key = 5;
}
message ProvisionerEnsureDataTableResponse {}
message ProvisionerMergeSiteSettingsRequest {
// JSON encoding of the defaults map; existing keys win.
bytes defaults_json = 1;
}
message ProvisionerMergeSiteSettingsResponse {}
message ProvisionerEnsureSettingRequest {
string key = 1;
// JSON encoding of the default value.
bytes default_value_json = 2;
}
message ProvisionerEnsureSettingResponse {}
// PageSeed mirrors plugin.PageConfig.
message PageSeed {
string slug = 1;
string parent_slug = 2;
string title = 3;
string template_key = 4;
repeated PageBlock blocks = 5;
string detail_source_type = 6;
string detail_source_key = 7;
string detail_slug_field = 8;
bool reconcile_blocks = 9;
bool reconcile_template = 10;
}
message ProvisionerEnsurePageRequest {
PageSeed page = 1;
}
message ProvisionerEnsurePageResponse {}
message ProvisionerOverrideSiteSettingsRequest {
// JSON encoding of the overrides map; plugin values win.
bytes overrides_json = 1;
}
message ProvisionerOverrideSiteSettingsResponse {}
// ProvisionerEnsureMenuItemRequest mirrors plugin.MenuItemConfig under a
// named menu.
message ProvisionerEnsureMenuItemRequest {
string menu_name = 1;
string label = 2;
string url = 3;
string page_slug = 4;
int32 sort_order = 5;
}
message ProvisionerEnsureMenuItemResponse {}
// ProvisionerRegisterEmbeddingConfigRequest mirrors plugin.EmbeddingConfigDef.
message ProvisionerRegisterEmbeddingConfigRequest {
string table_key = 1;
string text_template = 2;
bool enabled = 3;
}
message ProvisionerRegisterEmbeddingConfigResponse {}
// ProvisionerEnsureEmbedRequest mirrors plugin.EmbedConfig minus RenderFunc
// (function values cannot cross; the Template renders host-side).
message ProvisionerEnsureEmbedRequest {
string key = 1;
string title = 2;
string description = 3;
string icon = 4;
string label_field = 5;
string template = 6;
string data_source_type = 7; // EmbedDataSource.Type
string data_source_table_key = 8; // EmbedDataSource.TableKey
}
message ProvisionerEnsureEmbedResponse {}
// ProvisionerEnsureJobScheduleRequest mirrors plugin.JobScheduleConfig.
message ProvisionerEnsureJobScheduleRequest {
string job_type = 1;
string cron_expression = 2;
// JSON job configuration.
bytes config_json = 3;
}
message ProvisionerEnsureJobScheduleResponse {}
message ProvisionerUpdateDataTableRowFieldRequest {
string row_id = 1; // UUID
string field_key = 2;
// JSON encoding of the value.
bytes value_json = 3;
}
message ProvisionerUpdateDataTableRowFieldResponse {}
message ProvisionerDisableOrphanedJobSchedulesRequest {
repeated string registered_types = 1;
}
message ProvisionerDisableOrphanedJobSchedulesResponse {}
message ProvisionerEnsurePluginRequest {
string name = 1;
}
message ProvisionerEnsurePluginResponse {}
// ProvisionerEnsureCustomColorRequest mirrors plugin.CustomColorConfig.
message ProvisionerEnsureCustomColorRequest {
string name = 1;
string light_value = 2;
string dark_value = 3;
string source = 4;
}
message ProvisionerEnsureCustomColorResponse {}
// ProvisionerEnsureMediaRequest mirrors plugin.MediaDeposit with a REQUIRED
// deterministic id (the template-referable key). Idempotent: an existing id
// is a no-op; changed bytes warn rather than overwrite.
message ProvisionerEnsureMediaRequest {
string id = 1; // UUID, required
string filename = 2;
bytes data = 3;
string alt_text = 4;
string folder = 5;
string source = 6;
}
message ProvisionerEnsureMediaResponse {}
// --- settings.update_plugin_settings (settings.Updater, WO-WZ-019) ---
// SettingsUpdatePluginSettingsRequest replaces the calling plugin's own
// settings map. The host binds the target plugin from the caller's identity;
// plugin_name is advisory and MUST match it (PERMISSION_DENIED otherwise).
message SettingsUpdatePluginSettingsRequest {
string plugin_name = 1;
// JSON encoding of the full settings map.
bytes settings_json = 2;
}
message SettingsUpdatePluginSettingsResponse {}
// --- jobs.progress (plugin.JobHandlerFunc progress callback, WO-WZ-019) ---
// JobsProgressRequest reports progress for the CURRENTLY EXECUTING job. The
// host correlates it to the job via the invoking HOOK_JOB call's context, so
// it is only meaningful while a JOB hook is on the stack; outside one it is
// accepted and discarded.
message JobsProgressRequest {
int32 current = 1;
int32 total = 2;
string message = 3;
}
message JobsProgressResponse {}
// --- bridge.invoke (plugin.PluginBridge cross-plugin calls, WO-WZ-019) ---
// BridgeInvokeRequest calls a method on another plugin's registered bridge
// service. The provider answers via HOOK_BRIDGE_CALL (wasm) or an in-process
// plugin.BridgeInvokable (bundled). Payloads are opaque bytes — the two
// plugins agree on the encoding (JSON by convention).
message BridgeInvokeRequest {
string plugin_name = 1;
string service_name = 2;
string method = 3;
bytes payload = 4;
}
message BridgeInvokeResponse {
bytes payload = 1;
}