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>
190 lines
7.5 KiB
Go
190 lines
7.5 KiB
Go
package caps
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
|
|
abiv1 "git.dev.alexdunmow.com/block/pluginsdk/abi/v1"
|
|
"git.dev.alexdunmow.com/block/pluginsdk/plugin"
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// provisionerStub implements plugin.Provisioner over provisioner.* capability
|
|
// calls (WO-WZ-019). This is a LOAD-TIME capability: call it from the Load
|
|
// hook. RegisterWithProvisioner still receives a no-op at Register/DESCRIBE
|
|
// time (host functions are stubbed to fail there), so seed logic must move to
|
|
// Load — see core/docs/wasm-abi.md.
|
|
//
|
|
// Most Provisioner methods carry no context (the Go interface predates the
|
|
// ABI); calls run under the ambient invoke deadline the host enforces.
|
|
type provisionerStub struct{ base }
|
|
|
|
var _ plugin.Provisioner = (*provisionerStub)(nil)
|
|
|
|
func (s *provisionerStub) EnsureDataTable(config plugin.DataTableConfig) error {
|
|
req := &abiv1.ProvisionerEnsureDataTableRequest{
|
|
Key: config.Key,
|
|
Name: config.Name,
|
|
Description: config.Description,
|
|
SchemaJson: config.Schema,
|
|
PrimaryKey: config.PrimaryKey,
|
|
}
|
|
return s.invoke(context.Background(), "ensure_data_table", req, &abiv1.ProvisionerEnsureDataTableResponse{})
|
|
}
|
|
|
|
func (s *provisionerStub) MergeSiteSettings(defaults map[string]any) error {
|
|
defaultsJSON, err := json.Marshal(defaults)
|
|
if err != nil {
|
|
return fmt.Errorf("provisioner.merge_site_settings: marshal defaults: %w", err)
|
|
}
|
|
req := &abiv1.ProvisionerMergeSiteSettingsRequest{DefaultsJson: defaultsJSON}
|
|
return s.invoke(context.Background(), "merge_site_settings", req, &abiv1.ProvisionerMergeSiteSettingsResponse{})
|
|
}
|
|
|
|
func (s *provisionerStub) EnsureSetting(key string, defaultValue any) error {
|
|
valueJSON, err := json.Marshal(defaultValue)
|
|
if err != nil {
|
|
return fmt.Errorf("provisioner.ensure_setting: marshal value: %w", err)
|
|
}
|
|
req := &abiv1.ProvisionerEnsureSettingRequest{Key: key, DefaultValueJson: valueJSON}
|
|
return s.invoke(context.Background(), "ensure_setting", req, &abiv1.ProvisionerEnsureSettingResponse{})
|
|
}
|
|
|
|
func (s *provisionerStub) EnsurePage(config plugin.PageConfig) error {
|
|
seed := &abiv1.PageSeed{
|
|
Slug: config.Slug,
|
|
ParentSlug: config.ParentSlug,
|
|
Title: config.Title,
|
|
TemplateKey: config.TemplateKey,
|
|
DetailSourceType: config.DetailSourceType,
|
|
DetailSourceKey: config.DetailSourceKey,
|
|
DetailSlugField: config.DetailSlugField,
|
|
ReconcileBlocks: config.ReconcileBlocks,
|
|
ReconcileTemplate: config.ReconcileTemplate,
|
|
}
|
|
for _, b := range config.Blocks {
|
|
contentJSON, err := json.Marshal(b.Content)
|
|
if err != nil {
|
|
return fmt.Errorf("provisioner.ensure_page: block %q: marshal content: %w", b.BlockKey, err)
|
|
}
|
|
seed.Blocks = append(seed.Blocks, &abiv1.PageBlock{
|
|
BlockKey: b.BlockKey,
|
|
Title: b.Title,
|
|
ContentJson: contentJSON,
|
|
HtmlContent: b.HtmlContent,
|
|
Slot: b.Slot,
|
|
SortOrder: b.SortOrder,
|
|
})
|
|
}
|
|
req := &abiv1.ProvisionerEnsurePageRequest{Page: seed}
|
|
return s.invoke(context.Background(), "ensure_page", req, &abiv1.ProvisionerEnsurePageResponse{})
|
|
}
|
|
|
|
func (s *provisionerStub) OverrideSiteSettings(overrides map[string]any) error {
|
|
overridesJSON, err := json.Marshal(overrides)
|
|
if err != nil {
|
|
return fmt.Errorf("provisioner.override_site_settings: marshal overrides: %w", err)
|
|
}
|
|
req := &abiv1.ProvisionerOverrideSiteSettingsRequest{OverridesJson: overridesJSON}
|
|
return s.invoke(context.Background(), "override_site_settings", req, &abiv1.ProvisionerOverrideSiteSettingsResponse{})
|
|
}
|
|
|
|
func (s *provisionerStub) EnsureMenuItem(menuName string, config plugin.MenuItemConfig) error {
|
|
req := &abiv1.ProvisionerEnsureMenuItemRequest{
|
|
MenuName: menuName,
|
|
Label: config.Label,
|
|
Url: config.URL,
|
|
PageSlug: config.PageSlug,
|
|
SortOrder: config.SortOrder,
|
|
}
|
|
return s.invoke(context.Background(), "ensure_menu_item", req, &abiv1.ProvisionerEnsureMenuItemResponse{})
|
|
}
|
|
|
|
func (s *provisionerStub) RegisterEmbeddingConfig(config plugin.EmbeddingConfigDef) error {
|
|
req := &abiv1.ProvisionerRegisterEmbeddingConfigRequest{
|
|
TableKey: config.TableKey,
|
|
TextTemplate: config.TextTemplate,
|
|
Enabled: config.Enabled,
|
|
}
|
|
return s.invoke(context.Background(), "register_embedding_config", req, &abiv1.ProvisionerRegisterEmbeddingConfigResponse{})
|
|
}
|
|
|
|
// EnsureEmbed crosses the template-rendered surface only: EmbedConfig.
|
|
// RenderFunc is a function value that cannot serialize, so an ABI-provisioned
|
|
// embed must carry a Template (a RenderFunc-only embed returns an error
|
|
// rather than silently dropping the renderer).
|
|
func (s *provisionerStub) EnsureEmbed(config plugin.EmbedConfig) error {
|
|
if config.RenderFunc != nil && config.Template == "" {
|
|
return fmt.Errorf("provisioner.ensure_embed: embed %q uses RenderFunc, which cannot cross the wasm ABI — provide a Template instead", config.Key)
|
|
}
|
|
req := &abiv1.ProvisionerEnsureEmbedRequest{
|
|
Key: config.Key,
|
|
Title: config.Title,
|
|
Description: config.Description,
|
|
Icon: config.Icon,
|
|
LabelField: config.LabelField,
|
|
Template: config.Template,
|
|
DataSourceType: config.DataSource.Type,
|
|
DataSourceTableKey: config.DataSource.TableKey,
|
|
}
|
|
return s.invoke(context.Background(), "ensure_embed", req, &abiv1.ProvisionerEnsureEmbedResponse{})
|
|
}
|
|
|
|
func (s *provisionerStub) EnsureJobSchedule(config plugin.JobScheduleConfig) error {
|
|
req := &abiv1.ProvisionerEnsureJobScheduleRequest{
|
|
JobType: config.JobType,
|
|
CronExpression: config.CronExpression,
|
|
ConfigJson: config.Config,
|
|
}
|
|
return s.invoke(context.Background(), "ensure_job_schedule", req, &abiv1.ProvisionerEnsureJobScheduleResponse{})
|
|
}
|
|
|
|
func (s *provisionerStub) UpdateDataTableRowField(ctx context.Context, rowID uuid.UUID, fieldKey string, value any) error {
|
|
valueJSON, err := json.Marshal(value)
|
|
if err != nil {
|
|
return fmt.Errorf("provisioner.update_data_table_row_field: marshal value: %w", err)
|
|
}
|
|
req := &abiv1.ProvisionerUpdateDataTableRowFieldRequest{
|
|
RowId: rowID.String(),
|
|
FieldKey: fieldKey,
|
|
ValueJson: valueJSON,
|
|
}
|
|
return s.invoke(ctx, "update_data_table_row_field", req, &abiv1.ProvisionerUpdateDataTableRowFieldResponse{})
|
|
}
|
|
|
|
func (s *provisionerStub) DisableOrphanedJobSchedules(registeredTypes []string) error {
|
|
req := &abiv1.ProvisionerDisableOrphanedJobSchedulesRequest{RegisteredTypes: registeredTypes}
|
|
return s.invoke(context.Background(), "disable_orphaned_job_schedules", req, &abiv1.ProvisionerDisableOrphanedJobSchedulesResponse{})
|
|
}
|
|
|
|
func (s *provisionerStub) EnsurePlugin(name string) error {
|
|
req := &abiv1.ProvisionerEnsurePluginRequest{Name: name}
|
|
return s.invoke(context.Background(), "ensure_plugin", req, &abiv1.ProvisionerEnsurePluginResponse{})
|
|
}
|
|
|
|
func (s *provisionerStub) EnsureCustomColor(config plugin.CustomColorConfig) error {
|
|
req := &abiv1.ProvisionerEnsureCustomColorRequest{
|
|
Name: config.Name,
|
|
LightValue: config.LightValue,
|
|
DarkValue: config.DarkValue,
|
|
Source: config.Source,
|
|
}
|
|
return s.invoke(context.Background(), "ensure_custom_color", req, &abiv1.ProvisionerEnsureCustomColorResponse{})
|
|
}
|
|
|
|
func (s *provisionerStub) EnsureMedia(deposit plugin.MediaDeposit) error {
|
|
if deposit.ID == uuid.Nil {
|
|
return fmt.Errorf("provisioner.ensure_media: MediaDeposit.ID is required (the deterministic, template-referable key)")
|
|
}
|
|
req := &abiv1.ProvisionerEnsureMediaRequest{
|
|
Id: deposit.ID.String(),
|
|
Filename: deposit.Filename,
|
|
Data: deposit.Data,
|
|
AltText: deposit.AltText,
|
|
Folder: deposit.Folder,
|
|
Source: deposit.Source,
|
|
}
|
|
return s.invoke(context.Background(), "ensure_media", req, &abiv1.ProvisionerEnsureMediaResponse{})
|
|
}
|