feat(abi): injection-complete capability surface — provisioner family, content authoring, 4 callback hooks (WO-WZ-019)
Dynamic families added to the ABI + guest SDK: - provisioner.* (14 methods, 1:1 plugin.Provisioner): wasm provisioning was silently dead (RegisterWithProvisioner got a noopProvisioner and the cms loader ignored has_provisioner). Now a LOAD-TIME capability via the new CoreServices.Provisioner field; EnsureEmbed rejects RenderFunc-only embeds. - content.* writes (content.Author + CoreServices.ContentAuthor): create_page, set_page_blocks, publish_page, set_page_seo, upsert_post. - settings.update_plugin_settings (settings.Updater grows the method). - bridge.invoke + plugin.BridgeInvokable: opaque-payload cross-plugin calls (typed GetService still returns nil across the sandbox by design). - jobs.progress: HOOK_JOB handlers' progress() now crosses (was discarded). New host→guest hooks: HOOK_AI_TOOL_CALL (executes recorded ai.ToolDefinition handlers — registers tools in Register so every pooled instance has them), HOOK_BRIDGE_CALL, HOOK_DIRECTORY_PANEL_SECTION, HOOK_DIRECTORY_PIN_DECORATOR. The ai/bridge stubs now record handlers/values locally in addition to forwarding names. buf breaking clean (additive within ABI major 1); golden round-trips added for the new families (cms host replays the same goldens). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
6c52e3077c
commit
9e39119555
@ -556,3 +556,289 @@ message BadgesRefreshBadgesRequest {
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@ -49,6 +49,18 @@ enum Hook {
|
||||
// Apply a plugin-declared template filter (manifest.declared_filters):
|
||||
// payload = ApplyFilterRequest / ApplyFilterResponse.
|
||||
HOOK_APPLY_FILTER = 11;
|
||||
// Execute a guest-registered AI tool handler (ai.tools.register):
|
||||
// payload = AiToolCallRequest / AiToolCallResponse.
|
||||
HOOK_AI_TOOL_CALL = 12;
|
||||
// Invoke a method on a bridge service this plugin registered
|
||||
// (bridge.register_service): payload = BridgeCallRequest / BridgeCallResponse.
|
||||
HOOK_BRIDGE_CALL = 13;
|
||||
// Render one directory panel section (DirectoryExtensions.PanelSections):
|
||||
// payload = DirectoryPanelSectionRequest / DirectoryPanelSectionResponse.
|
||||
HOOK_DIRECTORY_PANEL_SECTION = 14;
|
||||
// Run one directory pin decorator (DirectoryExtensions.PinDecorators):
|
||||
// payload = DirectoryPinDecoratorRequest / DirectoryPinDecoratorResponse.
|
||||
HOOK_DIRECTORY_PIN_DECORATOR = 15;
|
||||
}
|
||||
|
||||
// InvokeRequest is the host→guest call envelope.
|
||||
@ -188,6 +200,70 @@ message MediaAnalyzedEvent {
|
||||
string safe_racy = 10;
|
||||
}
|
||||
|
||||
// --- AI_TOOL_CALL ---
|
||||
|
||||
// AiToolCallRequest executes the guest-side Handler of a tool the plugin
|
||||
// registered via ai.tools.register (ai.ToolHandler).
|
||||
message AiToolCallRequest {
|
||||
string slug = 1;
|
||||
// JSON encoding of the params map.
|
||||
bytes params_json = 2;
|
||||
}
|
||||
|
||||
// AiToolCallResponse mirrors ai.ToolResult. A handler-level failure travels
|
||||
// in error_message (a normal tool outcome the model sees); AbiError stays
|
||||
// reserved for transport/decode/panic failures.
|
||||
message AiToolCallResponse {
|
||||
string content = 1;
|
||||
string error_message = 2;
|
||||
}
|
||||
|
||||
// --- BRIDGE_CALL ---
|
||||
|
||||
// BridgeCallRequest asks THIS plugin (the provider) to run a method on one of
|
||||
// its registered bridge services. The consumer side is the bridge.invoke
|
||||
// capability; payload encoding is agreed between the two plugins (JSON by
|
||||
// convention).
|
||||
message BridgeCallRequest {
|
||||
string service_name = 1;
|
||||
string method = 2;
|
||||
bytes payload = 3;
|
||||
}
|
||||
|
||||
message BridgeCallResponse {
|
||||
bytes payload = 1;
|
||||
}
|
||||
|
||||
// --- DIRECTORY_PANEL_SECTION / DIRECTORY_PIN_DECORATOR ---
|
||||
|
||||
// DirectoryPanelSectionRequest renders the index-th registered panel section
|
||||
// (DirectoryExtensions.PanelSections[index], counted in
|
||||
// manifest.directory_extensions.panel_section_count).
|
||||
message DirectoryPanelSectionRequest {
|
||||
uint32 index = 1;
|
||||
// JSON encoding of the row data map.
|
||||
bytes data_json = 2;
|
||||
}
|
||||
|
||||
message DirectoryPanelSectionResponse {
|
||||
string html = 1;
|
||||
}
|
||||
|
||||
// DirectoryPinDecoratorRequest runs the index-th registered pin decorator,
|
||||
// which mutates the pin map in place; the mutated pin is returned.
|
||||
message DirectoryPinDecoratorRequest {
|
||||
uint32 index = 1;
|
||||
// JSON encoding of the pin map (mutated by the decorator).
|
||||
bytes pin_json = 2;
|
||||
// JSON encoding of the row data map (read-only input).
|
||||
bytes data_json = 3;
|
||||
}
|
||||
|
||||
message DirectoryPinDecoratorResponse {
|
||||
// JSON encoding of the mutated pin map.
|
||||
bytes pin_json = 1;
|
||||
}
|
||||
|
||||
// ModerationDecisionEvent mirrors plugin.ModerationDecisionEvent.
|
||||
message ModerationDecisionEvent {
|
||||
string media_id = 1;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -66,6 +66,18 @@ const (
|
||||
// Apply a plugin-declared template filter (manifest.declared_filters):
|
||||
// payload = ApplyFilterRequest / ApplyFilterResponse.
|
||||
Hook_HOOK_APPLY_FILTER Hook = 11
|
||||
// Execute a guest-registered AI tool handler (ai.tools.register):
|
||||
// payload = AiToolCallRequest / AiToolCallResponse.
|
||||
Hook_HOOK_AI_TOOL_CALL Hook = 12
|
||||
// Invoke a method on a bridge service this plugin registered
|
||||
// (bridge.register_service): payload = BridgeCallRequest / BridgeCallResponse.
|
||||
Hook_HOOK_BRIDGE_CALL Hook = 13
|
||||
// Render one directory panel section (DirectoryExtensions.PanelSections):
|
||||
// payload = DirectoryPanelSectionRequest / DirectoryPanelSectionResponse.
|
||||
Hook_HOOK_DIRECTORY_PANEL_SECTION Hook = 14
|
||||
// Run one directory pin decorator (DirectoryExtensions.PinDecorators):
|
||||
// payload = DirectoryPinDecoratorRequest / DirectoryPinDecoratorResponse.
|
||||
Hook_HOOK_DIRECTORY_PIN_DECORATOR Hook = 15
|
||||
)
|
||||
|
||||
// Enum value maps for Hook.
|
||||
@ -83,20 +95,28 @@ var (
|
||||
9: "HOOK_DESCRIBE",
|
||||
10: "HOOK_RENDER_TAG",
|
||||
11: "HOOK_APPLY_FILTER",
|
||||
12: "HOOK_AI_TOOL_CALL",
|
||||
13: "HOOK_BRIDGE_CALL",
|
||||
14: "HOOK_DIRECTORY_PANEL_SECTION",
|
||||
15: "HOOK_DIRECTORY_PIN_DECORATOR",
|
||||
}
|
||||
Hook_value = map[string]int32{
|
||||
"HOOK_UNSPECIFIED": 0,
|
||||
"HOOK_RENDER_BLOCK": 1,
|
||||
"HOOK_RENDER_TEMPLATE": 2,
|
||||
"HOOK_HANDLE_HTTP": 3,
|
||||
"HOOK_JOB": 4,
|
||||
"HOOK_LOAD": 5,
|
||||
"HOOK_UNLOAD": 6,
|
||||
"HOOK_RAG_FETCH": 7,
|
||||
"HOOK_MEDIA_HOOK": 8,
|
||||
"HOOK_DESCRIBE": 9,
|
||||
"HOOK_RENDER_TAG": 10,
|
||||
"HOOK_APPLY_FILTER": 11,
|
||||
"HOOK_UNSPECIFIED": 0,
|
||||
"HOOK_RENDER_BLOCK": 1,
|
||||
"HOOK_RENDER_TEMPLATE": 2,
|
||||
"HOOK_HANDLE_HTTP": 3,
|
||||
"HOOK_JOB": 4,
|
||||
"HOOK_LOAD": 5,
|
||||
"HOOK_UNLOAD": 6,
|
||||
"HOOK_RAG_FETCH": 7,
|
||||
"HOOK_MEDIA_HOOK": 8,
|
||||
"HOOK_DESCRIBE": 9,
|
||||
"HOOK_RENDER_TAG": 10,
|
||||
"HOOK_APPLY_FILTER": 11,
|
||||
"HOOK_AI_TOOL_CALL": 12,
|
||||
"HOOK_BRIDGE_CALL": 13,
|
||||
"HOOK_DIRECTORY_PANEL_SECTION": 14,
|
||||
"HOOK_DIRECTORY_PIN_DECORATOR": 15,
|
||||
}
|
||||
)
|
||||
|
||||
@ -1115,6 +1135,433 @@ func (x *MediaAnalyzedEvent) GetSafeRacy() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// AiToolCallRequest executes the guest-side Handler of a tool the plugin
|
||||
// registered via ai.tools.register (ai.ToolHandler).
|
||||
type AiToolCallRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Slug string `protobuf:"bytes,1,opt,name=slug,proto3" json:"slug,omitempty"`
|
||||
// JSON encoding of the params map.
|
||||
ParamsJson []byte `protobuf:"bytes,2,opt,name=params_json,json=paramsJson,proto3" json:"params_json,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *AiToolCallRequest) Reset() {
|
||||
*x = AiToolCallRequest{}
|
||||
mi := &file_v1_invoke_proto_msgTypes[17]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *AiToolCallRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*AiToolCallRequest) ProtoMessage() {}
|
||||
|
||||
func (x *AiToolCallRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_v1_invoke_proto_msgTypes[17]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use AiToolCallRequest.ProtoReflect.Descriptor instead.
|
||||
func (*AiToolCallRequest) Descriptor() ([]byte, []int) {
|
||||
return file_v1_invoke_proto_rawDescGZIP(), []int{17}
|
||||
}
|
||||
|
||||
func (x *AiToolCallRequest) GetSlug() string {
|
||||
if x != nil {
|
||||
return x.Slug
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *AiToolCallRequest) GetParamsJson() []byte {
|
||||
if x != nil {
|
||||
return x.ParamsJson
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AiToolCallResponse mirrors ai.ToolResult. A handler-level failure travels
|
||||
// in error_message (a normal tool outcome the model sees); AbiError stays
|
||||
// reserved for transport/decode/panic failures.
|
||||
type AiToolCallResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Content string `protobuf:"bytes,1,opt,name=content,proto3" json:"content,omitempty"`
|
||||
ErrorMessage string `protobuf:"bytes,2,opt,name=error_message,json=errorMessage,proto3" json:"error_message,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *AiToolCallResponse) Reset() {
|
||||
*x = AiToolCallResponse{}
|
||||
mi := &file_v1_invoke_proto_msgTypes[18]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *AiToolCallResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*AiToolCallResponse) ProtoMessage() {}
|
||||
|
||||
func (x *AiToolCallResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_v1_invoke_proto_msgTypes[18]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use AiToolCallResponse.ProtoReflect.Descriptor instead.
|
||||
func (*AiToolCallResponse) Descriptor() ([]byte, []int) {
|
||||
return file_v1_invoke_proto_rawDescGZIP(), []int{18}
|
||||
}
|
||||
|
||||
func (x *AiToolCallResponse) GetContent() string {
|
||||
if x != nil {
|
||||
return x.Content
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *AiToolCallResponse) GetErrorMessage() string {
|
||||
if x != nil {
|
||||
return x.ErrorMessage
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// BridgeCallRequest asks THIS plugin (the provider) to run a method on one of
|
||||
// its registered bridge services. The consumer side is the bridge.invoke
|
||||
// capability; payload encoding is agreed between the two plugins (JSON by
|
||||
// convention).
|
||||
type BridgeCallRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
ServiceName string `protobuf:"bytes,1,opt,name=service_name,json=serviceName,proto3" json:"service_name,omitempty"`
|
||||
Method string `protobuf:"bytes,2,opt,name=method,proto3" json:"method,omitempty"`
|
||||
Payload []byte `protobuf:"bytes,3,opt,name=payload,proto3" json:"payload,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *BridgeCallRequest) Reset() {
|
||||
*x = BridgeCallRequest{}
|
||||
mi := &file_v1_invoke_proto_msgTypes[19]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *BridgeCallRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*BridgeCallRequest) ProtoMessage() {}
|
||||
|
||||
func (x *BridgeCallRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_v1_invoke_proto_msgTypes[19]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use BridgeCallRequest.ProtoReflect.Descriptor instead.
|
||||
func (*BridgeCallRequest) Descriptor() ([]byte, []int) {
|
||||
return file_v1_invoke_proto_rawDescGZIP(), []int{19}
|
||||
}
|
||||
|
||||
func (x *BridgeCallRequest) GetServiceName() string {
|
||||
if x != nil {
|
||||
return x.ServiceName
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *BridgeCallRequest) GetMethod() string {
|
||||
if x != nil {
|
||||
return x.Method
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *BridgeCallRequest) GetPayload() []byte {
|
||||
if x != nil {
|
||||
return x.Payload
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type BridgeCallResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Payload []byte `protobuf:"bytes,1,opt,name=payload,proto3" json:"payload,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *BridgeCallResponse) Reset() {
|
||||
*x = BridgeCallResponse{}
|
||||
mi := &file_v1_invoke_proto_msgTypes[20]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *BridgeCallResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*BridgeCallResponse) ProtoMessage() {}
|
||||
|
||||
func (x *BridgeCallResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_v1_invoke_proto_msgTypes[20]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use BridgeCallResponse.ProtoReflect.Descriptor instead.
|
||||
func (*BridgeCallResponse) Descriptor() ([]byte, []int) {
|
||||
return file_v1_invoke_proto_rawDescGZIP(), []int{20}
|
||||
}
|
||||
|
||||
func (x *BridgeCallResponse) GetPayload() []byte {
|
||||
if x != nil {
|
||||
return x.Payload
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DirectoryPanelSectionRequest renders the index-th registered panel section
|
||||
// (DirectoryExtensions.PanelSections[index], counted in
|
||||
// manifest.directory_extensions.panel_section_count).
|
||||
type DirectoryPanelSectionRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Index uint32 `protobuf:"varint,1,opt,name=index,proto3" json:"index,omitempty"`
|
||||
// JSON encoding of the row data map.
|
||||
DataJson []byte `protobuf:"bytes,2,opt,name=data_json,json=dataJson,proto3" json:"data_json,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *DirectoryPanelSectionRequest) Reset() {
|
||||
*x = DirectoryPanelSectionRequest{}
|
||||
mi := &file_v1_invoke_proto_msgTypes[21]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *DirectoryPanelSectionRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*DirectoryPanelSectionRequest) ProtoMessage() {}
|
||||
|
||||
func (x *DirectoryPanelSectionRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_v1_invoke_proto_msgTypes[21]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use DirectoryPanelSectionRequest.ProtoReflect.Descriptor instead.
|
||||
func (*DirectoryPanelSectionRequest) Descriptor() ([]byte, []int) {
|
||||
return file_v1_invoke_proto_rawDescGZIP(), []int{21}
|
||||
}
|
||||
|
||||
func (x *DirectoryPanelSectionRequest) GetIndex() uint32 {
|
||||
if x != nil {
|
||||
return x.Index
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *DirectoryPanelSectionRequest) GetDataJson() []byte {
|
||||
if x != nil {
|
||||
return x.DataJson
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type DirectoryPanelSectionResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Html string `protobuf:"bytes,1,opt,name=html,proto3" json:"html,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *DirectoryPanelSectionResponse) Reset() {
|
||||
*x = DirectoryPanelSectionResponse{}
|
||||
mi := &file_v1_invoke_proto_msgTypes[22]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *DirectoryPanelSectionResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*DirectoryPanelSectionResponse) ProtoMessage() {}
|
||||
|
||||
func (x *DirectoryPanelSectionResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_v1_invoke_proto_msgTypes[22]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use DirectoryPanelSectionResponse.ProtoReflect.Descriptor instead.
|
||||
func (*DirectoryPanelSectionResponse) Descriptor() ([]byte, []int) {
|
||||
return file_v1_invoke_proto_rawDescGZIP(), []int{22}
|
||||
}
|
||||
|
||||
func (x *DirectoryPanelSectionResponse) GetHtml() string {
|
||||
if x != nil {
|
||||
return x.Html
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// DirectoryPinDecoratorRequest runs the index-th registered pin decorator,
|
||||
// which mutates the pin map in place; the mutated pin is returned.
|
||||
type DirectoryPinDecoratorRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Index uint32 `protobuf:"varint,1,opt,name=index,proto3" json:"index,omitempty"`
|
||||
// JSON encoding of the pin map (mutated by the decorator).
|
||||
PinJson []byte `protobuf:"bytes,2,opt,name=pin_json,json=pinJson,proto3" json:"pin_json,omitempty"`
|
||||
// JSON encoding of the row data map (read-only input).
|
||||
DataJson []byte `protobuf:"bytes,3,opt,name=data_json,json=dataJson,proto3" json:"data_json,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *DirectoryPinDecoratorRequest) Reset() {
|
||||
*x = DirectoryPinDecoratorRequest{}
|
||||
mi := &file_v1_invoke_proto_msgTypes[23]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *DirectoryPinDecoratorRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*DirectoryPinDecoratorRequest) ProtoMessage() {}
|
||||
|
||||
func (x *DirectoryPinDecoratorRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_v1_invoke_proto_msgTypes[23]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use DirectoryPinDecoratorRequest.ProtoReflect.Descriptor instead.
|
||||
func (*DirectoryPinDecoratorRequest) Descriptor() ([]byte, []int) {
|
||||
return file_v1_invoke_proto_rawDescGZIP(), []int{23}
|
||||
}
|
||||
|
||||
func (x *DirectoryPinDecoratorRequest) GetIndex() uint32 {
|
||||
if x != nil {
|
||||
return x.Index
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *DirectoryPinDecoratorRequest) GetPinJson() []byte {
|
||||
if x != nil {
|
||||
return x.PinJson
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *DirectoryPinDecoratorRequest) GetDataJson() []byte {
|
||||
if x != nil {
|
||||
return x.DataJson
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type DirectoryPinDecoratorResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
// JSON encoding of the mutated pin map.
|
||||
PinJson []byte `protobuf:"bytes,1,opt,name=pin_json,json=pinJson,proto3" json:"pin_json,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *DirectoryPinDecoratorResponse) Reset() {
|
||||
*x = DirectoryPinDecoratorResponse{}
|
||||
mi := &file_v1_invoke_proto_msgTypes[24]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *DirectoryPinDecoratorResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*DirectoryPinDecoratorResponse) ProtoMessage() {}
|
||||
|
||||
func (x *DirectoryPinDecoratorResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_v1_invoke_proto_msgTypes[24]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use DirectoryPinDecoratorResponse.ProtoReflect.Descriptor instead.
|
||||
func (*DirectoryPinDecoratorResponse) Descriptor() ([]byte, []int) {
|
||||
return file_v1_invoke_proto_rawDescGZIP(), []int{24}
|
||||
}
|
||||
|
||||
func (x *DirectoryPinDecoratorResponse) GetPinJson() []byte {
|
||||
if x != nil {
|
||||
return x.PinJson
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ModerationDecisionEvent mirrors plugin.ModerationDecisionEvent.
|
||||
type ModerationDecisionEvent struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
@ -1133,7 +1580,7 @@ type ModerationDecisionEvent struct {
|
||||
|
||||
func (x *ModerationDecisionEvent) Reset() {
|
||||
*x = ModerationDecisionEvent{}
|
||||
mi := &file_v1_invoke_proto_msgTypes[17]
|
||||
mi := &file_v1_invoke_proto_msgTypes[25]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@ -1145,7 +1592,7 @@ func (x *ModerationDecisionEvent) String() string {
|
||||
func (*ModerationDecisionEvent) ProtoMessage() {}
|
||||
|
||||
func (x *ModerationDecisionEvent) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_v1_invoke_proto_msgTypes[17]
|
||||
mi := &file_v1_invoke_proto_msgTypes[25]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@ -1158,7 +1605,7 @@ func (x *ModerationDecisionEvent) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use ModerationDecisionEvent.ProtoReflect.Descriptor instead.
|
||||
func (*ModerationDecisionEvent) Descriptor() ([]byte, []int) {
|
||||
return file_v1_invoke_proto_rawDescGZIP(), []int{17}
|
||||
return file_v1_invoke_proto_rawDescGZIP(), []int{25}
|
||||
}
|
||||
|
||||
func (x *ModerationDecisionEvent) GetMediaId() string {
|
||||
@ -1289,7 +1736,31 @@ const file_v1_invoke_proto_rawDesc = "" +
|
||||
"safe_adult\x18\b \x01(\tR\tsafeAdult\x12#\n" +
|
||||
"\rsafe_violence\x18\t \x01(\tR\fsafeViolence\x12\x1b\n" +
|
||||
"\tsafe_racy\x18\n" +
|
||||
" \x01(\tR\bsafeRacy\"\xb7\x02\n" +
|
||||
" \x01(\tR\bsafeRacy\"H\n" +
|
||||
"\x11AiToolCallRequest\x12\x12\n" +
|
||||
"\x04slug\x18\x01 \x01(\tR\x04slug\x12\x1f\n" +
|
||||
"\vparams_json\x18\x02 \x01(\fR\n" +
|
||||
"paramsJson\"S\n" +
|
||||
"\x12AiToolCallResponse\x12\x18\n" +
|
||||
"\acontent\x18\x01 \x01(\tR\acontent\x12#\n" +
|
||||
"\rerror_message\x18\x02 \x01(\tR\ferrorMessage\"h\n" +
|
||||
"\x11BridgeCallRequest\x12!\n" +
|
||||
"\fservice_name\x18\x01 \x01(\tR\vserviceName\x12\x16\n" +
|
||||
"\x06method\x18\x02 \x01(\tR\x06method\x12\x18\n" +
|
||||
"\apayload\x18\x03 \x01(\fR\apayload\".\n" +
|
||||
"\x12BridgeCallResponse\x12\x18\n" +
|
||||
"\apayload\x18\x01 \x01(\fR\apayload\"Q\n" +
|
||||
"\x1cDirectoryPanelSectionRequest\x12\x14\n" +
|
||||
"\x05index\x18\x01 \x01(\rR\x05index\x12\x1b\n" +
|
||||
"\tdata_json\x18\x02 \x01(\fR\bdataJson\"3\n" +
|
||||
"\x1dDirectoryPanelSectionResponse\x12\x12\n" +
|
||||
"\x04html\x18\x01 \x01(\tR\x04html\"l\n" +
|
||||
"\x1cDirectoryPinDecoratorRequest\x12\x14\n" +
|
||||
"\x05index\x18\x01 \x01(\rR\x05index\x12\x19\n" +
|
||||
"\bpin_json\x18\x02 \x01(\fR\apinJson\x12\x1b\n" +
|
||||
"\tdata_json\x18\x03 \x01(\fR\bdataJson\":\n" +
|
||||
"\x1dDirectoryPinDecoratorResponse\x12\x19\n" +
|
||||
"\bpin_json\x18\x01 \x01(\fR\apinJson\"\xb7\x02\n" +
|
||||
"\x17ModerationDecisionEvent\x12\x19\n" +
|
||||
"\bmedia_id\x18\x01 \x01(\tR\amediaId\x12\x1f\n" +
|
||||
"\vanalysis_id\x18\x02 \x01(\tR\n" +
|
||||
@ -1301,7 +1772,7 @@ const file_v1_invoke_proto_rawDesc = "" +
|
||||
"sourceType\x12\"\n" +
|
||||
"\rsource_ref_id\x18\a \x01(\tR\vsourceRefId\x12!\n" +
|
||||
"\fmoderated_by\x18\b \x01(\tR\vmoderatedBy\x12\x12\n" +
|
||||
"\x04note\x18\t \x01(\tR\x04note*\xf9\x01\n" +
|
||||
"\x04note\x18\t \x01(\tR\x04note*\xea\x02\n" +
|
||||
"\x04Hook\x12\x14\n" +
|
||||
"\x10HOOK_UNSPECIFIED\x10\x00\x12\x15\n" +
|
||||
"\x11HOOK_RENDER_BLOCK\x10\x01\x12\x18\n" +
|
||||
@ -1315,7 +1786,11 @@ const file_v1_invoke_proto_rawDesc = "" +
|
||||
"\rHOOK_DESCRIBE\x10\t\x12\x13\n" +
|
||||
"\x0fHOOK_RENDER_TAG\x10\n" +
|
||||
"\x12\x15\n" +
|
||||
"\x11HOOK_APPLY_FILTER\x10\v*\xf3\x01\n" +
|
||||
"\x11HOOK_APPLY_FILTER\x10\v\x12\x15\n" +
|
||||
"\x11HOOK_AI_TOOL_CALL\x10\f\x12\x14\n" +
|
||||
"\x10HOOK_BRIDGE_CALL\x10\r\x12 \n" +
|
||||
"\x1cHOOK_DIRECTORY_PANEL_SECTION\x10\x0e\x12 \n" +
|
||||
"\x1cHOOK_DIRECTORY_PIN_DECORATOR\x10\x0f*\xf3\x01\n" +
|
||||
"\fAbiErrorCode\x12\x1e\n" +
|
||||
"\x1aABI_ERROR_CODE_UNSPECIFIED\x10\x00\x12\x1b\n" +
|
||||
"\x17ABI_ERROR_CODE_INTERNAL\x10\x01\x12\x19\n" +
|
||||
@ -1338,38 +1813,46 @@ func file_v1_invoke_proto_rawDescGZIP() []byte {
|
||||
}
|
||||
|
||||
var file_v1_invoke_proto_enumTypes = make([]protoimpl.EnumInfo, 2)
|
||||
var file_v1_invoke_proto_msgTypes = make([]protoimpl.MessageInfo, 18)
|
||||
var file_v1_invoke_proto_msgTypes = make([]protoimpl.MessageInfo, 26)
|
||||
var file_v1_invoke_proto_goTypes = []any{
|
||||
(Hook)(0), // 0: abi.v1.Hook
|
||||
(AbiErrorCode)(0), // 1: abi.v1.AbiErrorCode
|
||||
(*InvokeRequest)(nil), // 2: abi.v1.InvokeRequest
|
||||
(*InvokeResponse)(nil), // 3: abi.v1.InvokeResponse
|
||||
(*AbiError)(nil), // 4: abi.v1.AbiError
|
||||
(*DescribeRequest)(nil), // 5: abi.v1.DescribeRequest
|
||||
(*DescribeResponse)(nil), // 6: abi.v1.DescribeResponse
|
||||
(*LoadRequest)(nil), // 7: abi.v1.LoadRequest
|
||||
(*HostConfig)(nil), // 8: abi.v1.HostConfig
|
||||
(*LoadResponse)(nil), // 9: abi.v1.LoadResponse
|
||||
(*UnloadRequest)(nil), // 10: abi.v1.UnloadRequest
|
||||
(*UnloadResponse)(nil), // 11: abi.v1.UnloadResponse
|
||||
(*JobRequest)(nil), // 12: abi.v1.JobRequest
|
||||
(*JobResponse)(nil), // 13: abi.v1.JobResponse
|
||||
(*RagFetchRequest)(nil), // 14: abi.v1.RagFetchRequest
|
||||
(*RagFetchResponse)(nil), // 15: abi.v1.RagFetchResponse
|
||||
(*MediaHookRequest)(nil), // 16: abi.v1.MediaHookRequest
|
||||
(*MediaHookResponse)(nil), // 17: abi.v1.MediaHookResponse
|
||||
(*MediaAnalyzedEvent)(nil), // 18: abi.v1.MediaAnalyzedEvent
|
||||
(*ModerationDecisionEvent)(nil), // 19: abi.v1.ModerationDecisionEvent
|
||||
(*PluginManifest)(nil), // 20: abi.v1.PluginManifest
|
||||
(Hook)(0), // 0: abi.v1.Hook
|
||||
(AbiErrorCode)(0), // 1: abi.v1.AbiErrorCode
|
||||
(*InvokeRequest)(nil), // 2: abi.v1.InvokeRequest
|
||||
(*InvokeResponse)(nil), // 3: abi.v1.InvokeResponse
|
||||
(*AbiError)(nil), // 4: abi.v1.AbiError
|
||||
(*DescribeRequest)(nil), // 5: abi.v1.DescribeRequest
|
||||
(*DescribeResponse)(nil), // 6: abi.v1.DescribeResponse
|
||||
(*LoadRequest)(nil), // 7: abi.v1.LoadRequest
|
||||
(*HostConfig)(nil), // 8: abi.v1.HostConfig
|
||||
(*LoadResponse)(nil), // 9: abi.v1.LoadResponse
|
||||
(*UnloadRequest)(nil), // 10: abi.v1.UnloadRequest
|
||||
(*UnloadResponse)(nil), // 11: abi.v1.UnloadResponse
|
||||
(*JobRequest)(nil), // 12: abi.v1.JobRequest
|
||||
(*JobResponse)(nil), // 13: abi.v1.JobResponse
|
||||
(*RagFetchRequest)(nil), // 14: abi.v1.RagFetchRequest
|
||||
(*RagFetchResponse)(nil), // 15: abi.v1.RagFetchResponse
|
||||
(*MediaHookRequest)(nil), // 16: abi.v1.MediaHookRequest
|
||||
(*MediaHookResponse)(nil), // 17: abi.v1.MediaHookResponse
|
||||
(*MediaAnalyzedEvent)(nil), // 18: abi.v1.MediaAnalyzedEvent
|
||||
(*AiToolCallRequest)(nil), // 19: abi.v1.AiToolCallRequest
|
||||
(*AiToolCallResponse)(nil), // 20: abi.v1.AiToolCallResponse
|
||||
(*BridgeCallRequest)(nil), // 21: abi.v1.BridgeCallRequest
|
||||
(*BridgeCallResponse)(nil), // 22: abi.v1.BridgeCallResponse
|
||||
(*DirectoryPanelSectionRequest)(nil), // 23: abi.v1.DirectoryPanelSectionRequest
|
||||
(*DirectoryPanelSectionResponse)(nil), // 24: abi.v1.DirectoryPanelSectionResponse
|
||||
(*DirectoryPinDecoratorRequest)(nil), // 25: abi.v1.DirectoryPinDecoratorRequest
|
||||
(*DirectoryPinDecoratorResponse)(nil), // 26: abi.v1.DirectoryPinDecoratorResponse
|
||||
(*ModerationDecisionEvent)(nil), // 27: abi.v1.ModerationDecisionEvent
|
||||
(*PluginManifest)(nil), // 28: abi.v1.PluginManifest
|
||||
}
|
||||
var file_v1_invoke_proto_depIdxs = []int32{
|
||||
0, // 0: abi.v1.InvokeRequest.hook:type_name -> abi.v1.Hook
|
||||
4, // 1: abi.v1.InvokeResponse.error:type_name -> abi.v1.AbiError
|
||||
1, // 2: abi.v1.AbiError.code:type_name -> abi.v1.AbiErrorCode
|
||||
20, // 3: abi.v1.DescribeResponse.manifest:type_name -> abi.v1.PluginManifest
|
||||
28, // 3: abi.v1.DescribeResponse.manifest:type_name -> abi.v1.PluginManifest
|
||||
8, // 4: abi.v1.LoadRequest.host_config:type_name -> abi.v1.HostConfig
|
||||
18, // 5: abi.v1.MediaHookRequest.media_analyzed:type_name -> abi.v1.MediaAnalyzedEvent
|
||||
19, // 6: abi.v1.MediaHookRequest.moderation_decision:type_name -> abi.v1.ModerationDecisionEvent
|
||||
27, // 6: abi.v1.MediaHookRequest.moderation_decision:type_name -> abi.v1.ModerationDecisionEvent
|
||||
7, // [7:7] is the sub-list for method output_type
|
||||
7, // [7:7] is the sub-list for method input_type
|
||||
7, // [7:7] is the sub-list for extension type_name
|
||||
@ -1393,7 +1876,7 @@ func file_v1_invoke_proto_init() {
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_v1_invoke_proto_rawDesc), len(file_v1_invoke_proto_rawDesc)),
|
||||
NumEnums: 2,
|
||||
NumMessages: 18,
|
||||
NumMessages: 26,
|
||||
NumExtensions: 0,
|
||||
NumServices: 0,
|
||||
},
|
||||
|
||||
86
content/author.go
Normal file
86
content/author.go
Normal file
@ -0,0 +1,86 @@
|
||||
package content
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// Author provides imperative page/post authoring for plugins (WO-WZ-019).
|
||||
// The CMS implements this interface and wires it into CoreServices.
|
||||
//
|
||||
// These are the runtime counterparts of the idempotent seed-time operations
|
||||
// on plugin.Provisioner: CreatePage is create-or-return-existing, the rest
|
||||
// mutate an existing page/post. Blog posts live in the CMS's dedicated
|
||||
// blog_posts table (not the pages table).
|
||||
type Author interface {
|
||||
CreatePage(ctx context.Context, params CreatePageParams) (CreatePageResult, error)
|
||||
SetPageBlocks(ctx context.Context, pageID uuid.UUID, blocks []PageBlock) error
|
||||
PublishPage(ctx context.Context, pageID uuid.UUID) error
|
||||
SetPageSEO(ctx context.Context, pageID uuid.UUID, seo PageSEO) error
|
||||
UpsertPost(ctx context.Context, params UpsertPostParams) (UpsertPostResult, error)
|
||||
}
|
||||
|
||||
// CreatePageParams describes a page to create.
|
||||
type CreatePageParams struct {
|
||||
Slug string
|
||||
ParentSlug string // "" = root
|
||||
Title string
|
||||
TemplateKey string
|
||||
MasterPageKey string // "" = none
|
||||
}
|
||||
|
||||
// CreatePageResult reports the created (or pre-existing) page.
|
||||
type CreatePageResult struct {
|
||||
PageID uuid.UUID
|
||||
// Created is false when a page with the slug already existed; the
|
||||
// existing ID is returned and nothing is modified.
|
||||
Created bool
|
||||
}
|
||||
|
||||
// PageBlock is one block instance placed on a page.
|
||||
type PageBlock struct {
|
||||
BlockKey string
|
||||
Title string
|
||||
Content map[string]any
|
||||
HTMLContent *string
|
||||
Slot string
|
||||
SortOrder int32
|
||||
}
|
||||
|
||||
// PageSEO carries the SEO fields settable on a page. Nil pointers leave the
|
||||
// existing value untouched.
|
||||
type PageSEO struct {
|
||||
MetaTitle *string
|
||||
MetaDescription *string
|
||||
OGTitle *string
|
||||
OGDescription *string
|
||||
OGImage *string
|
||||
FocusKeyphrase *string
|
||||
CanonicalURL *string
|
||||
RobotsDirective *string
|
||||
TwitterTitle *string
|
||||
TwitterDescription *string
|
||||
TwitterImage *string
|
||||
}
|
||||
|
||||
// UpsertPostParams creates or updates a blog post by slug.
|
||||
type UpsertPostParams struct {
|
||||
Slug string
|
||||
Title string
|
||||
Document map[string]any // BlockNote document
|
||||
Excerpt *string
|
||||
// AuthorProfileID / FeaturedImageID reference existing rows (e.g. a
|
||||
// media.deposit result for the image).
|
||||
AuthorProfileID *uuid.UUID
|
||||
FeaturedImageID *uuid.UUID
|
||||
// Publish publishes the post immediately after the upsert.
|
||||
Publish bool
|
||||
IsFeatured bool
|
||||
}
|
||||
|
||||
// UpsertPostResult reports the upserted post.
|
||||
type UpsertPostResult struct {
|
||||
PostID uuid.UUID
|
||||
Created bool
|
||||
}
|
||||
@ -1,10 +1,30 @@
|
||||
package plugin
|
||||
|
||||
import "context"
|
||||
|
||||
// PluginBridge allows plugins to share services with each other.
|
||||
// Plugins register named services during startup; other plugins look them up at runtime.
|
||||
//
|
||||
// In-process (bundled) plugins can share typed Go values via
|
||||
// RegisterService/GetService. Across the wasm sandbox a typed value cannot
|
||||
// cross, so cross-plugin calls go through Invoke instead: the provider's
|
||||
// service implements BridgeInvokable and the consumer calls Invoke with an
|
||||
// opaque payload (JSON by convention). GetService still reports availability
|
||||
// only (nil) for wasm consumers.
|
||||
type PluginBridge interface {
|
||||
RegisterService(pluginName, serviceName string, service any)
|
||||
GetService(pluginName, serviceName string) any
|
||||
// Invoke calls a method on another plugin's registered bridge service.
|
||||
// The provider answers via its BridgeInvokable implementation (bundled)
|
||||
// or the BRIDGE_CALL hook (wasm).
|
||||
Invoke(ctx context.Context, pluginName, serviceName, method string, payload []byte) ([]byte, error)
|
||||
}
|
||||
|
||||
// BridgeInvokable is implemented by bridge service values that support
|
||||
// method-by-name invocation with opaque payloads, making them callable from
|
||||
// other plugins across the wasm sandbox boundary.
|
||||
type BridgeInvokable interface {
|
||||
InvokeBridge(ctx context.Context, method string, payload []byte) ([]byte, error)
|
||||
}
|
||||
|
||||
// GetServiceAs retrieves a typed service from the bridge.
|
||||
|
||||
@ -20,6 +20,7 @@ import (
|
||||
type CoreServices struct {
|
||||
// Capability interfaces — typed access to CMS functionality
|
||||
Content content.Content
|
||||
ContentAuthor content.Author
|
||||
Settings settings.Settings
|
||||
Gating gating.Gating
|
||||
Crypto crypto.Crypto
|
||||
@ -61,6 +62,12 @@ type CoreServices struct {
|
||||
JobRunner JobRunner
|
||||
EmbeddingService EmbeddingService
|
||||
RAGService RAGService
|
||||
|
||||
// Provisioner — idempotent ensure/seed operations. In the wasm world this
|
||||
// is a LOAD-TIME capability: call it from the Load hook, not from
|
||||
// Register/RegisterWithProvisioner (DESCRIBE stubs every host function to
|
||||
// fail, so register-time provisioning cannot cross the ABI).
|
||||
Provisioner Provisioner
|
||||
}
|
||||
|
||||
// MediaDeposit describes an image a plugin deposits into the CMS media library.
|
||||
|
||||
@ -12,9 +12,15 @@ import (
|
||||
// CoreServices.AITextCall func field (ai.text_call).
|
||||
//
|
||||
// ToolDefinition.Handler stays guest-side: registration marshals only the
|
||||
// static descriptor. Host→guest tool execution is a runtime-WO concern
|
||||
// flagged in core/docs/wasm-abi.md.
|
||||
type aiStub struct{ base }
|
||||
// static descriptor AND records the full definition locally so the host can
|
||||
// execute the Handler via HOOK_AI_TOOL_CALL. Register tools in Register (every
|
||||
// pooled instance runs it), not Load (one instance only) — see
|
||||
// core/docs/wasm-abi.md §"Per-instance state". Instances are single-threaded,
|
||||
// so the plain map needs no locking.
|
||||
type aiStub struct {
|
||||
base
|
||||
tools map[string]*ai.ToolDefinition // slug → definition (with Handler)
|
||||
}
|
||||
|
||||
var _ ai.ToolRegistry = (*aiStub)(nil)
|
||||
|
||||
@ -24,6 +30,10 @@ func (s *aiStub) Register(tool *ai.ToolDefinition) {
|
||||
if tool == nil {
|
||||
return
|
||||
}
|
||||
if s.tools == nil {
|
||||
s.tools = make(map[string]*ai.ToolDefinition)
|
||||
}
|
||||
s.tools[tool.Slug] = tool
|
||||
req := &abiv1.AiToolRegisterRequest{
|
||||
Slug: tool.Slug,
|
||||
Name: tool.Name,
|
||||
@ -37,6 +47,13 @@ func (s *aiStub) Register(tool *ai.ToolDefinition) {
|
||||
_ = s.invoke(context.Background(), "tools.register", req, &abiv1.AiToolRegisterResponse{})
|
||||
}
|
||||
|
||||
// Tool returns the registered tool definition for a slug, for
|
||||
// HOOK_AI_TOOL_CALL dispatch by the wasm shim.
|
||||
func (s *aiStub) Tool(slug string) (*ai.ToolDefinition, bool) {
|
||||
t, ok := s.tools[slug]
|
||||
return t, ok
|
||||
}
|
||||
|
||||
// textCall backs CoreServices.AITextCall.
|
||||
func (s *aiStub) textCall(ctx context.Context, taskKey, systemPrompt, userMessage string) (string, error) {
|
||||
req := &abiv1.AiTextCallRequest{
|
||||
|
||||
120
plugin/wasmguest/caps/author.go
Normal file
120
plugin/wasmguest/caps/author.go
Normal file
@ -0,0 +1,120 @@
|
||||
package caps
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
abiv1 "git.dev.alexdunmow.com/block/core/abi/v1"
|
||||
"git.dev.alexdunmow.com/block/core/content"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// authorStub implements content.Author over the content.* write capability
|
||||
// calls (WO-WZ-019). Same family as contentStub; split so the read surface
|
||||
// stays untouched.
|
||||
type authorStub struct{ base }
|
||||
|
||||
var _ content.Author = (*authorStub)(nil)
|
||||
|
||||
func (s *authorStub) CreatePage(ctx context.Context, params content.CreatePageParams) (content.CreatePageResult, error) {
|
||||
req := &abiv1.ContentCreatePageRequest{
|
||||
Slug: params.Slug,
|
||||
ParentSlug: params.ParentSlug,
|
||||
Title: params.Title,
|
||||
TemplateKey: params.TemplateKey,
|
||||
MasterPageKey: params.MasterPageKey,
|
||||
}
|
||||
resp := &abiv1.ContentCreatePageResponse{}
|
||||
if err := s.invoke(ctx, "create_page", req, resp); err != nil {
|
||||
return content.CreatePageResult{}, err
|
||||
}
|
||||
id, err := uuid.Parse(resp.GetPageId())
|
||||
if err != nil {
|
||||
return content.CreatePageResult{}, fmt.Errorf("content.create_page: bad page_id %q: %w", resp.GetPageId(), err)
|
||||
}
|
||||
return content.CreatePageResult{PageID: id, Created: resp.GetCreated()}, nil
|
||||
}
|
||||
|
||||
func (s *authorStub) SetPageBlocks(ctx context.Context, pageID uuid.UUID, blocks []content.PageBlock) error {
|
||||
req := &abiv1.ContentSetPageBlocksRequest{PageId: pageID.String()}
|
||||
for _, b := range blocks {
|
||||
pb, err := pageBlockToProto(b)
|
||||
if err != nil {
|
||||
return fmt.Errorf("content.set_page_blocks: block %q: %w", b.BlockKey, err)
|
||||
}
|
||||
req.Blocks = append(req.Blocks, pb)
|
||||
}
|
||||
return s.invoke(ctx, "set_page_blocks", req, &abiv1.ContentSetPageBlocksResponse{})
|
||||
}
|
||||
|
||||
func (s *authorStub) PublishPage(ctx context.Context, pageID uuid.UUID) error {
|
||||
req := &abiv1.ContentPublishPageRequest{PageId: pageID.String()}
|
||||
return s.invoke(ctx, "publish_page", req, &abiv1.ContentPublishPageResponse{})
|
||||
}
|
||||
|
||||
func (s *authorStub) SetPageSEO(ctx context.Context, pageID uuid.UUID, seo content.PageSEO) error {
|
||||
req := &abiv1.ContentSetPageSeoRequest{
|
||||
PageId: pageID.String(),
|
||||
MetaTitle: seo.MetaTitle,
|
||||
MetaDescription: seo.MetaDescription,
|
||||
OgTitle: seo.OGTitle,
|
||||
OgDescription: seo.OGDescription,
|
||||
OgImage: seo.OGImage,
|
||||
FocusKeyphrase: seo.FocusKeyphrase,
|
||||
CanonicalUrl: seo.CanonicalURL,
|
||||
RobotsDirective: seo.RobotsDirective,
|
||||
TwitterTitle: seo.TwitterTitle,
|
||||
TwitterDescription: seo.TwitterDescription,
|
||||
TwitterImage: seo.TwitterImage,
|
||||
}
|
||||
return s.invoke(ctx, "set_page_seo", req, &abiv1.ContentSetPageSeoResponse{})
|
||||
}
|
||||
|
||||
func (s *authorStub) UpsertPost(ctx context.Context, params content.UpsertPostParams) (content.UpsertPostResult, error) {
|
||||
docJSON, err := json.Marshal(params.Document)
|
||||
if err != nil {
|
||||
return content.UpsertPostResult{}, fmt.Errorf("content.upsert_post: marshal document: %w", err)
|
||||
}
|
||||
req := &abiv1.ContentUpsertPostRequest{
|
||||
Slug: params.Slug,
|
||||
Title: params.Title,
|
||||
DocumentJson: docJSON,
|
||||
Excerpt: params.Excerpt,
|
||||
Publish: params.Publish,
|
||||
IsFeatured: params.IsFeatured,
|
||||
}
|
||||
if params.AuthorProfileID != nil {
|
||||
v := params.AuthorProfileID.String()
|
||||
req.AuthorProfileId = &v
|
||||
}
|
||||
if params.FeaturedImageID != nil {
|
||||
v := params.FeaturedImageID.String()
|
||||
req.FeaturedImageId = &v
|
||||
}
|
||||
resp := &abiv1.ContentUpsertPostResponse{}
|
||||
if err := s.invoke(ctx, "upsert_post", req, resp); err != nil {
|
||||
return content.UpsertPostResult{}, err
|
||||
}
|
||||
id, err := uuid.Parse(resp.GetPostId())
|
||||
if err != nil {
|
||||
return content.UpsertPostResult{}, fmt.Errorf("content.upsert_post: bad post_id %q: %w", resp.GetPostId(), err)
|
||||
}
|
||||
return content.UpsertPostResult{PostID: id, Created: resp.GetCreated()}, nil
|
||||
}
|
||||
|
||||
// pageBlockToProto converts one content.PageBlock to its wire form.
|
||||
func pageBlockToProto(b content.PageBlock) (*abiv1.PageBlock, error) {
|
||||
contentJSON, err := json.Marshal(b.Content)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &abiv1.PageBlock{
|
||||
BlockKey: b.BlockKey,
|
||||
Title: b.Title,
|
||||
ContentJson: contentJSON,
|
||||
HtmlContent: b.HTMLContent,
|
||||
Slot: b.Slot,
|
||||
SortOrder: b.SortOrder,
|
||||
}, nil
|
||||
}
|
||||
@ -11,25 +11,64 @@ import (
|
||||
//
|
||||
// The bridge shares in-process Go values today; across sandboxes only the
|
||||
// registration/lookup *surface* serializes — the service value itself cannot
|
||||
// cross. RegisterService therefore forwards only plugin/service names (the
|
||||
// value is dropped), and GetService returns nil (availability is checked but a
|
||||
// typed value cannot be reconstructed guest-side). Typed cross-plugin
|
||||
// invocation is an open ABI item (core/docs/wasm-abi.md).
|
||||
type bridgeStub struct{ base }
|
||||
// cross. RegisterService therefore forwards only plugin/service names to the
|
||||
// host AND records the value locally so this plugin's own services are
|
||||
// dispatchable via HOOK_BRIDGE_CALL; GetService returns nil for remote
|
||||
// services (availability is checked but a typed value cannot be reconstructed
|
||||
// guest-side). Cross-plugin calls use Invoke (bridge.invoke) with opaque
|
||||
// payloads instead.
|
||||
//
|
||||
// Instances are single-threaded, so the plain map needs no locking; register
|
||||
// bridge services in Register (every pooled instance runs it), not Load
|
||||
// (which fires on one instance only) — see core/docs/wasm-abi.md
|
||||
// §"Per-instance state".
|
||||
type bridgeStub struct {
|
||||
base
|
||||
services map[string]any // serviceName → registered value (this plugin's own)
|
||||
}
|
||||
|
||||
var _ plugin.PluginBridge = (*bridgeStub)(nil)
|
||||
|
||||
// RegisterService has no error channel; a transport failure is dropped.
|
||||
func (s *bridgeStub) RegisterService(pluginName, serviceName string, _ any) {
|
||||
func (s *bridgeStub) RegisterService(pluginName, serviceName string, service any) {
|
||||
if s.services == nil {
|
||||
s.services = make(map[string]any)
|
||||
}
|
||||
s.services[serviceName] = service
|
||||
req := &abiv1.BridgeRegisterServiceRequest{PluginName: pluginName, ServiceName: serviceName}
|
||||
_ = s.invoke(context.Background(), "register_service", req, &abiv1.BridgeRegisterServiceResponse{})
|
||||
}
|
||||
|
||||
// GetService reports availability host-side but cannot return the concrete Go
|
||||
// value across the sandbox boundary, so it always returns nil. Callers using
|
||||
// plugin.GetServiceAs correctly observe (zero, false).
|
||||
// value across the sandbox boundary, so it always returns nil for remote
|
||||
// services. Callers using plugin.GetServiceAs correctly observe (zero, false);
|
||||
// cross-plugin calls go through Invoke.
|
||||
func (s *bridgeStub) GetService(pluginName, serviceName string) any {
|
||||
req := &abiv1.BridgeGetServiceRequest{PluginName: pluginName, ServiceName: serviceName}
|
||||
_ = s.invoke(context.Background(), "get_service", req, &abiv1.BridgeGetServiceResponse{})
|
||||
return nil
|
||||
}
|
||||
|
||||
// Invoke calls a method on another plugin's registered bridge service with an
|
||||
// opaque payload (bridge.invoke; the provider answers via HOOK_BRIDGE_CALL or
|
||||
// its in-process plugin.BridgeInvokable).
|
||||
func (s *bridgeStub) Invoke(ctx context.Context, pluginName, serviceName, method string, payload []byte) ([]byte, error) {
|
||||
req := &abiv1.BridgeInvokeRequest{
|
||||
PluginName: pluginName,
|
||||
ServiceName: serviceName,
|
||||
Method: method,
|
||||
Payload: payload,
|
||||
}
|
||||
resp := &abiv1.BridgeInvokeResponse{}
|
||||
if err := s.invoke(ctx, "invoke", req, resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp.GetPayload(), nil
|
||||
}
|
||||
|
||||
// Service returns this plugin's own registered service value, for
|
||||
// HOOK_BRIDGE_CALL dispatch by the wasm shim.
|
||||
func (s *bridgeStub) Service(serviceName string) (any, bool) {
|
||||
v, ok := s.services[serviceName]
|
||||
return v, ok
|
||||
}
|
||||
|
||||
@ -611,6 +611,137 @@ func TestCapabilityRoundTrip(t *testing.T) {
|
||||
}
|
||||
},
|
||||
},
|
||||
// --- content writes (WO-WZ-019) ---
|
||||
{
|
||||
name: "content_create_page", wantMethod: "content.create_page",
|
||||
resp: &abiv1.ContentCreatePageResponse{PageId: idItem.String(), Created: true},
|
||||
run: func(t *testing.T, cs plugin.CoreServices) {
|
||||
got, err := cs.ContentAuthor.CreatePage(ctx, content.CreatePageParams{
|
||||
Slug: "/pricing", ParentSlug: "", Title: "Pricing", TemplateKey: "landing",
|
||||
})
|
||||
if err != nil || got.PageID != idItem || !got.Created {
|
||||
t.Errorf("create_page = %+v err = %v", got, err)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "content_set_page_blocks", wantMethod: "content.set_page_blocks",
|
||||
resp: &abiv1.ContentSetPageBlocksResponse{},
|
||||
run: func(t *testing.T, cs plugin.CoreServices) {
|
||||
html := "<h1>Hi</h1>"
|
||||
err := cs.ContentAuthor.SetPageBlocks(ctx, idItem, []content.PageBlock{
|
||||
{BlockKey: "html", Title: "Hero", Content: map[string]any{"align": "center"}, HTMLContent: &html, Slot: "main", SortOrder: 1},
|
||||
{BlockKey: "cta", Title: "CTA", Content: map[string]any{"label": "Go"}, SortOrder: 2},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "content_publish_page", wantMethod: "content.publish_page",
|
||||
resp: &abiv1.ContentPublishPageResponse{},
|
||||
run: func(t *testing.T, cs plugin.CoreServices) {
|
||||
if err := cs.ContentAuthor.PublishPage(ctx, idItem); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "content_set_page_seo", wantMethod: "content.set_page_seo",
|
||||
resp: &abiv1.ContentSetPageSeoResponse{},
|
||||
run: func(t *testing.T, cs plugin.CoreServices) {
|
||||
mt, md := "Pricing — Acme", "Plans and pricing."
|
||||
err := cs.ContentAuthor.SetPageSEO(ctx, idItem, content.PageSEO{MetaTitle: &mt, MetaDescription: &md})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "content_upsert_post", wantMethod: "content.upsert_post",
|
||||
resp: &abiv1.ContentUpsertPostResponse{PostId: idRow.String(), Created: false},
|
||||
run: func(t *testing.T, cs plugin.CoreServices) {
|
||||
excerpt := "hello"
|
||||
got, err := cs.ContentAuthor.UpsertPost(ctx, content.UpsertPostParams{
|
||||
Slug: "hello", Title: "Hello", Document: map[string]any{"type": "doc"},
|
||||
Excerpt: &excerpt, AuthorProfileID: &idAuthor, FeaturedImageID: &idMedia,
|
||||
Publish: true,
|
||||
})
|
||||
if err != nil || got.PostID != idRow || got.Created {
|
||||
t.Errorf("upsert_post = %+v err = %v", got, err)
|
||||
}
|
||||
},
|
||||
},
|
||||
// --- settings.update_plugin_settings (WO-WZ-019) ---
|
||||
{
|
||||
name: "settings_update_plugin_settings", wantMethod: "settings.update_plugin_settings",
|
||||
resp: &abiv1.SettingsUpdatePluginSettingsResponse{},
|
||||
run: func(t *testing.T, cs plugin.CoreServices) {
|
||||
err := cs.SettingsUpdater.UpdatePluginSettings(ctx, "myplugin", map[string]any{"enabled": true})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
},
|
||||
},
|
||||
// --- provisioner (WO-WZ-019) ---
|
||||
{
|
||||
name: "provisioner_ensure_page", wantMethod: "provisioner.ensure_page",
|
||||
resp: &abiv1.ProvisionerEnsurePageResponse{},
|
||||
run: func(t *testing.T, cs plugin.CoreServices) {
|
||||
err := cs.Provisioner.EnsurePage(plugin.PageConfig{
|
||||
Slug: "/", Title: "Home", TemplateKey: "homepage", ReconcileBlocks: true,
|
||||
Blocks: []plugin.PageBlockConfig{
|
||||
{BlockKey: "html", Title: "Hero", Content: map[string]any{"x": float64(1)}, Slot: "main", SortOrder: 1},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "provisioner_merge_site_settings", wantMethod: "provisioner.merge_site_settings",
|
||||
resp: &abiv1.ProvisionerMergeSiteSettingsResponse{},
|
||||
run: func(t *testing.T, cs plugin.CoreServices) {
|
||||
if err := cs.Provisioner.MergeSiteSettings(map[string]any{"site_name": "Acme"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "provisioner_ensure_menu_item", wantMethod: "provisioner.ensure_menu_item",
|
||||
resp: &abiv1.ProvisionerEnsureMenuItemResponse{},
|
||||
run: func(t *testing.T, cs plugin.CoreServices) {
|
||||
err := cs.Provisioner.EnsureMenuItem("main", plugin.MenuItemConfig{Label: "Docs", PageSlug: "/docs", SortOrder: 3})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "provisioner_ensure_media", wantMethod: "provisioner.ensure_media",
|
||||
resp: &abiv1.ProvisionerEnsureMediaResponse{},
|
||||
run: func(t *testing.T, cs plugin.CoreServices) {
|
||||
err := cs.Provisioner.EnsureMedia(plugin.MediaDeposit{
|
||||
ID: idMedia, Filename: "hero.jpg", Data: []byte{1, 2, 3}, AltText: "hero", Folder: "seed", Source: "myplugin",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
},
|
||||
},
|
||||
// --- bridge.invoke (WO-WZ-019) ---
|
||||
{
|
||||
name: "bridge_invoke", wantMethod: "bridge.invoke",
|
||||
resp: &abiv1.BridgeInvokeResponse{Payload: []byte(`{"ok":true}`)},
|
||||
run: func(t *testing.T, cs plugin.CoreServices) {
|
||||
got, err := cs.Bridge.Invoke(ctx, "symposium", "seeder", "seed_forum", []byte(`{"n":1}`))
|
||||
if err != nil || string(got) != `{"ok":true}` {
|
||||
t.Errorf("bridge.invoke = %q err = %v", got, err)
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
|
||||
@ -22,11 +22,12 @@ import (
|
||||
// - MediaPath / AppURL → delivered in LoadRequest.host_config at load
|
||||
// - CoreServiceBindings → static manifest.core_service_bindings; host mounts
|
||||
func NewCoreServices(call CallFunc) plugin.CoreServices {
|
||||
settings := &settingsStub{base{family: "settings", call: call}}
|
||||
ai := &aiStub{base{family: "ai", call: call}}
|
||||
settings := &settingsStub{base: base{family: "settings", call: call}}
|
||||
ai := &aiStub{base: base{family: "ai", call: call}}
|
||||
|
||||
return plugin.CoreServices{
|
||||
Content: &contentStub{base{family: "content", call: call}},
|
||||
ContentAuthor: &authorStub{base{family: "content", call: call}},
|
||||
Settings: settings,
|
||||
SettingsUpdater: settings,
|
||||
Gating: &gatingStub{base{family: "gating", call: call}},
|
||||
@ -39,11 +40,12 @@ func NewCoreServices(call CallFunc) plugin.CoreServices {
|
||||
ToolRegistry: ai,
|
||||
AITextCall: ai.textCall,
|
||||
EmailSender: &emailStub{base{family: "email", call: call}},
|
||||
Bridge: &bridgeStub{base{family: "bridge", call: call}},
|
||||
Bridge: &bridgeStub{base: base{family: "bridge", call: call}},
|
||||
ReviewSubmitter: &reviewsStub{base{family: "reviews", call: call}},
|
||||
BadgeRefresher: &badgesStub{base{family: "badges", call: call}},
|
||||
JobRunner: &jobsStub{base{family: "jobs", call: call}},
|
||||
EmbeddingService: &embeddingsStub{base{family: "embeddings", call: call}},
|
||||
RAGService: NewRAGStub(call),
|
||||
Provisioner: &provisionerStub{base{family: "provisioner", call: call}},
|
||||
}
|
||||
}
|
||||
|
||||
189
plugin/wasmguest/caps/provisioner.go
Normal file
189
plugin/wasmguest/caps/provisioner.go
Normal file
@ -0,0 +1,189 @@
|
||||
package caps
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
abiv1 "git.dev.alexdunmow.com/block/core/abi/v1"
|
||||
"git.dev.alexdunmow.com/block/core/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{})
|
||||
}
|
||||
@ -42,3 +42,12 @@ func (s *settingsStub) UpdateSiteSetting(ctx context.Context, key string, value
|
||||
req := &abiv1.SettingsUpdateSiteSettingRequest{Key: key, ValueJson: valueJSON}
|
||||
return s.invoke(ctx, "update_site_setting", req, &abiv1.SettingsUpdateSiteSettingResponse{})
|
||||
}
|
||||
|
||||
func (s *settingsStub) UpdatePluginSettings(ctx context.Context, pluginName string, settingsMap map[string]any) error {
|
||||
settingsJSON, err := json.Marshal(settingsMap)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req := &abiv1.SettingsUpdatePluginSettingsRequest{PluginName: pluginName, SettingsJson: settingsJSON}
|
||||
return s.invoke(ctx, "update_plugin_settings", req, &abiv1.SettingsUpdatePluginSettingsResponse{})
|
||||
}
|
||||
|
||||
3
plugin/wasmguest/caps/testdata/golden/bridge_invoke_req.pb
vendored
Normal file
3
plugin/wasmguest/caps/testdata/golden/bridge_invoke_req.pb
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
|
||||
symposiumseeder
|
||||
seed_forum"{"n":1}
|
||||
2
plugin/wasmguest/caps/testdata/golden/bridge_invoke_resp.pb
vendored
Normal file
2
plugin/wasmguest/caps/testdata/golden/bridge_invoke_resp.pb
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
|
||||
{"ok":true}
|
||||
2
plugin/wasmguest/caps/testdata/golden/content_create_page_req.pb
vendored
Normal file
2
plugin/wasmguest/caps/testdata/golden/content_create_page_req.pb
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
|
||||
/pricingPricing"landing
|
||||
2
plugin/wasmguest/caps/testdata/golden/content_create_page_resp.pb
vendored
Normal file
2
plugin/wasmguest/caps/testdata/golden/content_create_page_resp.pb
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
|
||||
$44444444-4444-4444-4444-444444444444
|
||||
2
plugin/wasmguest/caps/testdata/golden/content_publish_page_req.pb
vendored
Normal file
2
plugin/wasmguest/caps/testdata/golden/content_publish_page_req.pb
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
|
||||
$44444444-4444-4444-4444-444444444444
|
||||
0
plugin/wasmguest/caps/testdata/golden/content_publish_page_resp.pb
vendored
Normal file
0
plugin/wasmguest/caps/testdata/golden/content_publish_page_resp.pb
vendored
Normal file
4
plugin/wasmguest/caps/testdata/golden/content_set_page_blocks_req.pb
vendored
Normal file
4
plugin/wasmguest/caps/testdata/golden/content_set_page_blocks_req.pb
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
|
||||
$44444444-4444-4444-4444-4444444444445
|
||||
htmlHero{"align":"center"}"<h1>Hi</h1>*main0
|
||||
ctaCTA{"label":"Go"}0
|
||||
0
plugin/wasmguest/caps/testdata/golden/content_set_page_blocks_resp.pb
vendored
Normal file
0
plugin/wasmguest/caps/testdata/golden/content_set_page_blocks_resp.pb
vendored
Normal file
2
plugin/wasmguest/caps/testdata/golden/content_set_page_seo_req.pb
vendored
Normal file
2
plugin/wasmguest/caps/testdata/golden/content_set_page_seo_req.pb
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
|
||||
$44444444-4444-4444-4444-444444444444Pricing — AcmePlans and pricing.
|
||||
0
plugin/wasmguest/caps/testdata/golden/content_set_page_seo_resp.pb
vendored
Normal file
0
plugin/wasmguest/caps/testdata/golden/content_set_page_seo_resp.pb
vendored
Normal file
2
plugin/wasmguest/caps/testdata/golden/content_upsert_post_req.pb
vendored
Normal file
2
plugin/wasmguest/caps/testdata/golden/content_upsert_post_req.pb
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
|
||||
helloHello{"type":"doc"}"hello*$22222222-2222-2222-2222-2222222222222$99999999-9999-9999-9999-9999999999998
|
||||
2
plugin/wasmguest/caps/testdata/golden/content_upsert_post_resp.pb
vendored
Normal file
2
plugin/wasmguest/caps/testdata/golden/content_upsert_post_resp.pb
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
|
||||
$bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb
|
||||
2
plugin/wasmguest/caps/testdata/golden/provisioner_ensure_media_req.pb
vendored
Normal file
2
plugin/wasmguest/caps/testdata/golden/provisioner_ensure_media_req.pb
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
|
||||
$99999999-9999-9999-9999-999999999999hero.jpg"hero*seed2myplugin
|
||||
0
plugin/wasmguest/caps/testdata/golden/provisioner_ensure_media_resp.pb
vendored
Normal file
0
plugin/wasmguest/caps/testdata/golden/provisioner_ensure_media_resp.pb
vendored
Normal file
2
plugin/wasmguest/caps/testdata/golden/provisioner_ensure_menu_item_req.pb
vendored
Normal file
2
plugin/wasmguest/caps/testdata/golden/provisioner_ensure_menu_item_req.pb
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
|
||||
mainDocs"/docs(
|
||||
0
plugin/wasmguest/caps/testdata/golden/provisioner_ensure_menu_item_resp.pb
vendored
Normal file
0
plugin/wasmguest/caps/testdata/golden/provisioner_ensure_menu_item_resp.pb
vendored
Normal file
4
plugin/wasmguest/caps/testdata/golden/provisioner_ensure_page_req.pb
vendored
Normal file
4
plugin/wasmguest/caps/testdata/golden/provisioner_ensure_page_req.pb
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
|
||||
4
|
||||
/Home"homepage*
|
||||
htmlHero{"x":1}*main0H
|
||||
0
plugin/wasmguest/caps/testdata/golden/provisioner_ensure_page_resp.pb
vendored
Normal file
0
plugin/wasmguest/caps/testdata/golden/provisioner_ensure_page_resp.pb
vendored
Normal file
2
plugin/wasmguest/caps/testdata/golden/provisioner_merge_site_settings_req.pb
vendored
Normal file
2
plugin/wasmguest/caps/testdata/golden/provisioner_merge_site_settings_req.pb
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
|
||||
{"site_name":"Acme"}
|
||||
0
plugin/wasmguest/caps/testdata/golden/provisioner_merge_site_settings_resp.pb
vendored
Normal file
0
plugin/wasmguest/caps/testdata/golden/provisioner_merge_site_settings_resp.pb
vendored
Normal file
2
plugin/wasmguest/caps/testdata/golden/settings_update_plugin_settings_req.pb
vendored
Normal file
2
plugin/wasmguest/caps/testdata/golden/settings_update_plugin_settings_req.pb
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
|
||||
myplugin{"enabled":true}
|
||||
0
plugin/wasmguest/caps/testdata/golden/settings_update_plugin_settings_resp.pb
vendored
Normal file
0
plugin/wasmguest/caps/testdata/golden/settings_update_plugin_settings_resp.pb
vendored
Normal file
@ -12,6 +12,7 @@ import (
|
||||
"time"
|
||||
|
||||
abiv1 "git.dev.alexdunmow.com/block/core/abi/v1"
|
||||
"git.dev.alexdunmow.com/block/core/ai"
|
||||
"git.dev.alexdunmow.com/block/core/blocks"
|
||||
"git.dev.alexdunmow.com/block/core/plugin"
|
||||
"git.dev.alexdunmow.com/block/core/plugin/wasmguest/bnwasm"
|
||||
@ -102,8 +103,11 @@ func newGuest(reg plugin.PluginRegistration) *guest {
|
||||
}
|
||||
|
||||
// runRegister invokes Register (or RegisterWithProvisioner with a no-op
|
||||
// provisioner — provisioning itself runs host-side at load) against capture
|
||||
// registries, converting panics to errors.
|
||||
// provisioner) against capture registries, converting panics to errors.
|
||||
// Register-time provisioning cannot cross the ABI (DESCRIBE stubs every host
|
||||
// function to fail), so RegisterWithProvisioner's provisioner is inert here;
|
||||
// wasm plugins provision from Load via deps.Provisioner (the provisioner.*
|
||||
// capability family, WO-WZ-019) instead.
|
||||
func runRegister(reg plugin.PluginRegistration, tr *captureTemplateRegistry, br *captureBlockRegistry) (err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
@ -186,6 +190,14 @@ func (g *guest) invoke(hookID uint32, req []byte) (out []byte) {
|
||||
payload, abiErr = g.ragFetch(ctx, env.GetPayload())
|
||||
case abiv1.Hook_HOOK_MEDIA_HOOK:
|
||||
payload, abiErr = g.mediaHook(ctx, env.GetPayload())
|
||||
case abiv1.Hook_HOOK_AI_TOOL_CALL:
|
||||
payload, abiErr = g.aiToolCall(ctx, env.GetPayload())
|
||||
case abiv1.Hook_HOOK_BRIDGE_CALL:
|
||||
payload, abiErr = g.bridgeCall(ctx, env.GetPayload())
|
||||
case abiv1.Hook_HOOK_DIRECTORY_PANEL_SECTION:
|
||||
payload, abiErr = g.directoryPanelSection(env.GetPayload())
|
||||
case abiv1.Hook_HOOK_DIRECTORY_PIN_DECORATOR:
|
||||
payload, abiErr = g.directoryPinDecorator(env.GetPayload())
|
||||
default:
|
||||
abiErr = &abiv1.AbiError{
|
||||
Code: abiv1.AbiErrorCode_ABI_ERROR_CODE_UNIMPLEMENTED,
|
||||
@ -390,9 +402,16 @@ func (g *guest) runJob(ctx context.Context, payload []byte) (proto.Message, *abi
|
||||
Message: "unknown job type " + req.GetJobType(),
|
||||
}
|
||||
}
|
||||
// Job progress reporting needs a jobs.progress host function — an ABI
|
||||
// open item (core/docs/wasm-abi.md); v1 discards progress updates.
|
||||
progress := func(current, total int, message string) {}
|
||||
// Progress crosses as best-effort jobs.progress host calls; the host
|
||||
// correlates them to the running job via the HOOK_JOB call's context. A
|
||||
// nil transport (native builds) discards updates.
|
||||
progress := func(current, total int, message string) {
|
||||
if capTransport == nil {
|
||||
return
|
||||
}
|
||||
preq := &abiv1.JobsProgressRequest{Current: int32(current), Total: int32(total), Message: message}
|
||||
_ = capTransport("jobs.progress", preq, &abiv1.JobsProgressResponse{})
|
||||
}
|
||||
result, err := handler(ctx, req.GetConfigJson(), progress)
|
||||
if err != nil {
|
||||
return nil, internalError(err.Error())
|
||||
@ -533,6 +552,163 @@ func (g *guest) ragFetcher(contentType string) (plugin.ContentFetcher, bool) {
|
||||
return g.rag.Fetcher(contentType)
|
||||
}
|
||||
|
||||
// toolLookup / serviceLookup are the recording-stub accessors dispatch needs
|
||||
// for HOOK_AI_TOOL_CALL / HOOK_BRIDGE_CALL. The caps stubs implement them;
|
||||
// asserting interfaces keeps the stub types unexported.
|
||||
type toolLookup interface {
|
||||
Tool(slug string) (*ai.ToolDefinition, bool)
|
||||
}
|
||||
|
||||
type serviceLookup interface {
|
||||
Service(serviceName string) (any, bool)
|
||||
}
|
||||
|
||||
// aiToolCall executes a guest-registered AI tool handler (HOOK_AI_TOOL_CALL).
|
||||
// Tools must be registered in Register (every pooled instance runs it) for the
|
||||
// lookup to succeed on any instance — see wasm-abi.md §"Per-instance state".
|
||||
func (g *guest) aiToolCall(ctx context.Context, payload []byte) (proto.Message, *abiv1.AbiError) {
|
||||
req := &abiv1.AiToolCallRequest{}
|
||||
if err := proto.Unmarshal(payload, req); err != nil {
|
||||
return nil, decodeError("AiToolCallRequest", err)
|
||||
}
|
||||
reg, ok := g.services.ToolRegistry.(toolLookup)
|
||||
if !ok {
|
||||
return nil, &abiv1.AbiError{
|
||||
Code: abiv1.AbiErrorCode_ABI_ERROR_CODE_UNIMPLEMENTED,
|
||||
Message: "tool registry does not record handlers",
|
||||
}
|
||||
}
|
||||
tool, ok := reg.Tool(req.GetSlug())
|
||||
if !ok || tool.Handler == nil {
|
||||
return nil, &abiv1.AbiError{
|
||||
Code: abiv1.AbiErrorCode_ABI_ERROR_CODE_UNIMPLEMENTED,
|
||||
Message: "no handler for AI tool " + req.GetSlug() + " on this instance (register tools in Register, not Load)",
|
||||
}
|
||||
}
|
||||
var params map[string]any
|
||||
if raw := req.GetParamsJson(); len(raw) > 0 {
|
||||
if err := json.Unmarshal(raw, ¶ms); err != nil {
|
||||
return nil, decodeError("AiToolCallRequest.params_json", err)
|
||||
}
|
||||
}
|
||||
result, err := tool.Handler(ctx, params)
|
||||
if err != nil {
|
||||
return nil, internalError(err.Error())
|
||||
}
|
||||
resp := &abiv1.AiToolCallResponse{}
|
||||
if result != nil {
|
||||
resp.Content = result.Content
|
||||
resp.ErrorMessage = result.Error
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// bridgeCall invokes a method on one of THIS plugin's registered bridge
|
||||
// services (HOOK_BRIDGE_CALL; the consumer side is the bridge.invoke
|
||||
// capability). The service value must implement plugin.BridgeInvokable and be
|
||||
// registered in Register so every pooled instance has it.
|
||||
func (g *guest) bridgeCall(ctx context.Context, payload []byte) (proto.Message, *abiv1.AbiError) {
|
||||
req := &abiv1.BridgeCallRequest{}
|
||||
if err := proto.Unmarshal(payload, req); err != nil {
|
||||
return nil, decodeError("BridgeCallRequest", err)
|
||||
}
|
||||
lookup, ok := g.services.Bridge.(serviceLookup)
|
||||
if !ok {
|
||||
return nil, &abiv1.AbiError{
|
||||
Code: abiv1.AbiErrorCode_ABI_ERROR_CODE_UNIMPLEMENTED,
|
||||
Message: "bridge does not record services",
|
||||
}
|
||||
}
|
||||
svc, ok := lookup.Service(req.GetServiceName())
|
||||
if !ok {
|
||||
return nil, &abiv1.AbiError{
|
||||
Code: abiv1.AbiErrorCode_ABI_ERROR_CODE_UNIMPLEMENTED,
|
||||
Message: "no bridge service " + req.GetServiceName() + " on this instance (register services in Register, not Load)",
|
||||
}
|
||||
}
|
||||
invokable, ok := svc.(plugin.BridgeInvokable)
|
||||
if !ok {
|
||||
return nil, &abiv1.AbiError{
|
||||
Code: abiv1.AbiErrorCode_ABI_ERROR_CODE_UNIMPLEMENTED,
|
||||
Message: "bridge service " + req.GetServiceName() + " does not implement plugin.BridgeInvokable",
|
||||
}
|
||||
}
|
||||
out, err := invokable.InvokeBridge(ctx, req.GetMethod(), req.GetPayload())
|
||||
if err != nil {
|
||||
return nil, internalError(err.Error())
|
||||
}
|
||||
return &abiv1.BridgeCallResponse{Payload: out}, nil
|
||||
}
|
||||
|
||||
// directoryExtensions resolves the registration's DirectoryExtensions value,
|
||||
// nil when the plugin declares none.
|
||||
func (g *guest) directoryExtensions() *plugin.DirectoryExtensions {
|
||||
if g.reg.DirectoryExtensions == nil {
|
||||
return nil
|
||||
}
|
||||
return g.reg.DirectoryExtensions()
|
||||
}
|
||||
|
||||
// directoryPanelSection renders the index-th registered panel section
|
||||
// (HOOK_DIRECTORY_PANEL_SECTION).
|
||||
func (g *guest) directoryPanelSection(payload []byte) (proto.Message, *abiv1.AbiError) {
|
||||
req := &abiv1.DirectoryPanelSectionRequest{}
|
||||
if err := proto.Unmarshal(payload, req); err != nil {
|
||||
return nil, decodeError("DirectoryPanelSectionRequest", err)
|
||||
}
|
||||
ext := g.directoryExtensions()
|
||||
idx := int(req.GetIndex())
|
||||
if ext == nil || idx >= len(ext.PanelSections) || ext.PanelSections[idx] == nil {
|
||||
return nil, &abiv1.AbiError{
|
||||
Code: abiv1.AbiErrorCode_ABI_ERROR_CODE_UNIMPLEMENTED,
|
||||
Message: fmt.Sprintf("no directory panel section at index %d", idx),
|
||||
}
|
||||
}
|
||||
var data map[string]any
|
||||
if raw := req.GetDataJson(); len(raw) > 0 {
|
||||
if err := json.Unmarshal(raw, &data); err != nil {
|
||||
return nil, decodeError("DirectoryPanelSectionRequest.data_json", err)
|
||||
}
|
||||
}
|
||||
return &abiv1.DirectoryPanelSectionResponse{Html: ext.PanelSections[idx](data)}, nil
|
||||
}
|
||||
|
||||
// directoryPinDecorator runs the index-th registered pin decorator, which
|
||||
// mutates the pin map in place; the mutated pin is returned
|
||||
// (HOOK_DIRECTORY_PIN_DECORATOR).
|
||||
func (g *guest) directoryPinDecorator(payload []byte) (proto.Message, *abiv1.AbiError) {
|
||||
req := &abiv1.DirectoryPinDecoratorRequest{}
|
||||
if err := proto.Unmarshal(payload, req); err != nil {
|
||||
return nil, decodeError("DirectoryPinDecoratorRequest", err)
|
||||
}
|
||||
ext := g.directoryExtensions()
|
||||
idx := int(req.GetIndex())
|
||||
if ext == nil || idx >= len(ext.PinDecorators) || ext.PinDecorators[idx] == nil {
|
||||
return nil, &abiv1.AbiError{
|
||||
Code: abiv1.AbiErrorCode_ABI_ERROR_CODE_UNIMPLEMENTED,
|
||||
Message: fmt.Sprintf("no directory pin decorator at index %d", idx),
|
||||
}
|
||||
}
|
||||
pin := map[string]any{}
|
||||
if raw := req.GetPinJson(); len(raw) > 0 {
|
||||
if err := json.Unmarshal(raw, &pin); err != nil {
|
||||
return nil, decodeError("DirectoryPinDecoratorRequest.pin_json", err)
|
||||
}
|
||||
}
|
||||
var data map[string]any
|
||||
if raw := req.GetDataJson(); len(raw) > 0 {
|
||||
if err := json.Unmarshal(raw, &data); err != nil {
|
||||
return nil, decodeError("DirectoryPinDecoratorRequest.data_json", err)
|
||||
}
|
||||
}
|
||||
ext.PinDecorators[idx](pin, data)
|
||||
pinJSON, err := json.Marshal(pin)
|
||||
if err != nil {
|
||||
return nil, internalError("marshal mutated pin: " + err.Error())
|
||||
}
|
||||
return &abiv1.DirectoryPinDecoratorResponse{PinJson: pinJSON}, nil
|
||||
}
|
||||
|
||||
// sortedKeys returns the map's keys in sorted order (deterministic manifests).
|
||||
func sortedKeys[V any](m map[string]V) []string {
|
||||
keys := make([]string, 0, len(m))
|
||||
|
||||
@ -8,9 +8,13 @@ type Settings interface {
|
||||
GetPluginSettings(ctx context.Context, pluginName string) (map[string]any, error)
|
||||
}
|
||||
|
||||
// Updater allows plugins to modify site settings.
|
||||
// Updater allows plugins to modify site settings and their own plugin
|
||||
// settings.
|
||||
type Updater interface {
|
||||
UpdateSiteSetting(ctx context.Context, key string, value any) error
|
||||
// UpdatePluginSettings replaces the plugin's own settings map. Across the
|
||||
// wasm ABI the host enforces that pluginName matches the calling plugin.
|
||||
UpdatePluginSettings(ctx context.Context, pluginName string, settings map[string]any) error
|
||||
}
|
||||
|
||||
// GetStringOr returns a string value from a map, or defaultVal if not found/wrong type.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user