Compare commits

..

7 Commits
v0.3.2 ... main

Author SHA1 Message Date
Alex Dunmow
5f0cc20bbf chore: checkpoint all working changes 2026-09-05 21:14:15 +08:00
Alex Dunmow
f14846d758 fix(wasmguest): expose host services during register 2026-08-25 12:42:42 +08:00
Alex Dunmow
e2dcbc1fc3 feat: authenticate bridge callers and declare SDK compatibility 2026-08-20 00:24:49 +08:00
Alex Dunmow
731d6b51d1 feat(plugin): declare admin API compatibility 2026-08-19 23:21:47 +08:00
Alex Dunmow
abef5b70da fix(render): reject active-content link schemes 2026-08-19 22:44:40 +08:00
Alex Dunmow
fb58679b6f feat(abi): add keyed page and render response contracts 2026-08-19 21:50:28 +08:00
Alex Dunmow
301f286c2f feat(plugin): add durable keyed load-once hooks 2026-08-10 23:04:47 +08:00
47 changed files with 1701 additions and 355 deletions

View File

@ -770,6 +770,7 @@ message ProvisionerEnsureSettingResponse {}
// PageSeed mirrors plugin.PageConfig. // PageSeed mirrors plugin.PageConfig.
message PageSeed { message PageSeed {
string slug = 1; string slug = 1;
// Legacy slug-based parent reference. Prefer parent_key for new page seeds.
string parent_slug = 2; string parent_slug = 2;
string title = 3; string title = 3;
string template_key = 4; string template_key = 4;
@ -779,6 +780,16 @@ message PageSeed {
string detail_slug_field = 8; string detail_slug_field = 8;
bool reconcile_blocks = 9; bool reconcile_blocks = 9;
bool reconcile_template = 10; bool reconcile_template = 10;
// Stable identity scoped to the declaring plugin.
string key = 11;
// Stable identity of a parent page declared by the same plugin.
string parent_key = 12;
// Applied only when the page is created; later reconciliation does not move
// an existing page when this value changes.
string mount_path = 13;
// Shallowly add absent top-level defaults to matching plugin-owned block
// assignments while preserving all present values, including empty values.
bool merge_missing_block_content = 14;
} }
message ProvisionerEnsurePageRequest { message ProvisionerEnsurePageRequest {
@ -802,6 +813,10 @@ message ProvisionerEnsureMenuItemRequest {
string url = 3; string url = 3;
string page_slug = 4; string page_slug = 4;
int32 sort_order = 5; int32 sort_order = 5;
// Stable plugin/page-key target. When set, these identify the provisioned
// page independently of its current slug.
string page_plugin = 6;
string page_key = 7;
} }
message ProvisionerEnsureMenuItemResponse {} message ProvisionerEnsureMenuItemResponse {}

View File

@ -61,6 +61,9 @@ enum Hook {
// Run one directory pin decorator (DirectoryExtensions.PinDecorators): // Run one directory pin decorator (DirectoryExtensions.PinDecorators):
// payload = DirectoryPinDecoratorRequest / DirectoryPinDecoratorResponse. // payload = DirectoryPinDecoratorRequest / DirectoryPinDecoratorResponse.
HOOK_DIRECTORY_PIN_DECORATOR = 15; HOOK_DIRECTORY_PIN_DECORATOR = 15;
// Run one keyed PluginRegistration.LoadOnce callback:
// payload = LoadOnceRequest / LoadOnceResponse.
HOOK_LOAD_ONCE = 16;
} }
// InvokeRequest is the hostguest call envelope. // InvokeRequest is the hostguest call envelope.
@ -145,6 +148,15 @@ message HostConfig {
message LoadResponse {} message LoadResponse {}
// LoadOnceRequest identifies the callback selected by the host from the
// manifest's load_once_keys. The host records the key only after a successful
// response; failed callbacks may be retried on a later load.
message LoadOnceRequest {
string key = 1;
}
message LoadOnceResponse {}
message UnloadRequest {} message UnloadRequest {}
message UnloadResponse {} message UnloadResponse {}
@ -234,6 +246,9 @@ message BridgeCallRequest {
string service_name = 1; string service_name = 1;
string method = 2; string method = 2;
bytes payload = 3; bytes payload = 3;
// Host-authenticated identity of the consumer plugin. The host derives this
// from the loaded caller; providers must not trust identity in payload.
string caller_plugin = 4;
} }
message BridgeCallResponse { message BridgeCallResponse {

View File

@ -164,6 +164,12 @@ message PluginManifest {
// every procedure against rbac_method_roles before advertising or invoking // every procedure against rbac_method_roles before advertising or invoking
// it, so descriptor metadata cannot widen the plugin's authorization map. // it, so descriptor metadata cannot widen the plugin's authorization map.
repeated MCPToolDescriptor mcp_tools = 38; repeated MCPToolDescriptor mcp_tools = 38;
// Stable keys for PluginRegistration.LoadOnce callbacks. The host persists
// successful (plugin,key) executions and invokes HOOK_LOAD_ONCE only for
// keys it has never completed. Changing a key is the explicit migration
// mechanism for running a replacement callback once.
repeated string load_once_keys = 39;
} }
// MCPToolDescriptor is one unary ConnectRPC method prepared for the host MCP // MCPToolDescriptor is one unary ConnectRPC method prepared for the host MCP

View File

@ -44,6 +44,23 @@ message PoweredBlock {
string template = 1; string template = 1;
// JSON encoding of the template data map (map[string]any). // JSON encoding of the template data map (map[string]any).
bytes data_json = 2; bytes data_json = 2;
// Requested HTTP response status. Before writing headers, the host accepts
// only 200599, uses the highest status class, and keeps the first result
// encountered within that class.
int32 status_code = 3;
// Structured document metadata merged by the host into the page head. The
// first non-empty value for each field wins in deterministic render order.
DocumentMetadata metadata = 4;
}
// DocumentMetadata is the safe, shared subset of document head data a plugin
// may provide. The host validates and escapes every value before rendering.
message DocumentMetadata {
string title = 1;
string description = 2;
string canonical_url = 3;
string image_url = 4;
string robots = 5;
} }
// RenderTemplateRequest renders one template (templates.TemplateFunc). // RenderTemplateRequest renders one template (templates.TemplateFunc).
@ -76,6 +93,11 @@ message TemplateDocument {
// Extra <html>-element attributes (e.g. data-theme). A "lang" key here is // Extra <html>-element attributes (e.g. data-theme). A "lang" key here is
// ignored the lang field wins. // ignored the lang field wins.
map<string, string> html_attrs = 6; map<string, string> html_attrs = 6;
// Requested HTTP response status. The host applies the same validation and
// precedence rules as PoweredBlock.status_code.
int32 status_code = 7;
// Structured document metadata merged by the host into the page head.
DocumentMetadata metadata = 8;
} }
// RenderTagRequest invokes a plugin-declared template tag (HOOK_RENDER_TAG). // RenderTagRequest invokes a plugin-declared template tag (HOOK_RENDER_TAG).
@ -181,6 +203,10 @@ message PageContext {
string title = 3; string title = 3;
string post_type = 4; // "page", "post", "master", "system" string post_type = 4; // "page", "post", "master", "system"
string status = 5; // "published", "draft", "scheduled" string status = 5; // "published", "draft", "scheduled"
// Slug derived from the resolved page hierarchy.
string derived_slug = 6;
// Effective mount path after host inheritance and normalization.
string mount_path = 7;
} }
// PostContext mirrors blocks.PostContext. // PostContext mirrors blocks.PostContext.
@ -276,4 +302,8 @@ message BlockContext {
bytes detail_row_data_json = 36; bytes detail_row_data_json = 36;
string blog_index_url = 37; string blog_index_url = 37;
string category_page_url = 38; string category_page_url = 38;
// Slug derived from the resolved page hierarchy.
string derived_slug = 39;
// Effective mount path after host inheritance and normalization.
string mount_path = 40;
} }

View File

@ -6670,6 +6670,7 @@ func (*ProvisionerEnsureSettingResponse) Descriptor() ([]byte, []int) {
type PageSeed struct { type PageSeed struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
Slug string `protobuf:"bytes,1,opt,name=slug,proto3" json:"slug,omitempty"` Slug string `protobuf:"bytes,1,opt,name=slug,proto3" json:"slug,omitempty"`
// Legacy slug-based parent reference. Prefer parent_key for new page seeds.
ParentSlug string `protobuf:"bytes,2,opt,name=parent_slug,json=parentSlug,proto3" json:"parent_slug,omitempty"` ParentSlug string `protobuf:"bytes,2,opt,name=parent_slug,json=parentSlug,proto3" json:"parent_slug,omitempty"`
Title string `protobuf:"bytes,3,opt,name=title,proto3" json:"title,omitempty"` Title string `protobuf:"bytes,3,opt,name=title,proto3" json:"title,omitempty"`
TemplateKey string `protobuf:"bytes,4,opt,name=template_key,json=templateKey,proto3" json:"template_key,omitempty"` TemplateKey string `protobuf:"bytes,4,opt,name=template_key,json=templateKey,proto3" json:"template_key,omitempty"`
@ -6679,6 +6680,16 @@ type PageSeed struct {
DetailSlugField string `protobuf:"bytes,8,opt,name=detail_slug_field,json=detailSlugField,proto3" json:"detail_slug_field,omitempty"` DetailSlugField string `protobuf:"bytes,8,opt,name=detail_slug_field,json=detailSlugField,proto3" json:"detail_slug_field,omitempty"`
ReconcileBlocks bool `protobuf:"varint,9,opt,name=reconcile_blocks,json=reconcileBlocks,proto3" json:"reconcile_blocks,omitempty"` ReconcileBlocks bool `protobuf:"varint,9,opt,name=reconcile_blocks,json=reconcileBlocks,proto3" json:"reconcile_blocks,omitempty"`
ReconcileTemplate bool `protobuf:"varint,10,opt,name=reconcile_template,json=reconcileTemplate,proto3" json:"reconcile_template,omitempty"` ReconcileTemplate bool `protobuf:"varint,10,opt,name=reconcile_template,json=reconcileTemplate,proto3" json:"reconcile_template,omitempty"`
// Stable identity scoped to the declaring plugin.
Key string `protobuf:"bytes,11,opt,name=key,proto3" json:"key,omitempty"`
// Stable identity of a parent page declared by the same plugin.
ParentKey string `protobuf:"bytes,12,opt,name=parent_key,json=parentKey,proto3" json:"parent_key,omitempty"`
// Applied only when the page is created; later reconciliation does not move
// an existing page when this value changes.
MountPath string `protobuf:"bytes,13,opt,name=mount_path,json=mountPath,proto3" json:"mount_path,omitempty"`
// Shallowly add absent top-level defaults to matching plugin-owned block
// assignments while preserving all present values, including empty values.
MergeMissingBlockContent bool `protobuf:"varint,14,opt,name=merge_missing_block_content,json=mergeMissingBlockContent,proto3" json:"merge_missing_block_content,omitempty"`
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
} }
@ -6783,6 +6794,34 @@ func (x *PageSeed) GetReconcileTemplate() bool {
return false return false
} }
func (x *PageSeed) GetKey() string {
if x != nil {
return x.Key
}
return ""
}
func (x *PageSeed) GetParentKey() string {
if x != nil {
return x.ParentKey
}
return ""
}
func (x *PageSeed) GetMountPath() string {
if x != nil {
return x.MountPath
}
return ""
}
func (x *PageSeed) GetMergeMissingBlockContent() bool {
if x != nil {
return x.MergeMissingBlockContent
}
return false
}
type ProvisionerEnsurePageRequest struct { type ProvisionerEnsurePageRequest struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
Page *PageSeed `protobuf:"bytes,1,opt,name=page,proto3" json:"page,omitempty"` Page *PageSeed `protobuf:"bytes,1,opt,name=page,proto3" json:"page,omitempty"`
@ -6953,6 +6992,10 @@ type ProvisionerEnsureMenuItemRequest struct {
Url string `protobuf:"bytes,3,opt,name=url,proto3" json:"url,omitempty"` Url string `protobuf:"bytes,3,opt,name=url,proto3" json:"url,omitempty"`
PageSlug string `protobuf:"bytes,4,opt,name=page_slug,json=pageSlug,proto3" json:"page_slug,omitempty"` PageSlug string `protobuf:"bytes,4,opt,name=page_slug,json=pageSlug,proto3" json:"page_slug,omitempty"`
SortOrder int32 `protobuf:"varint,5,opt,name=sort_order,json=sortOrder,proto3" json:"sort_order,omitempty"` SortOrder int32 `protobuf:"varint,5,opt,name=sort_order,json=sortOrder,proto3" json:"sort_order,omitempty"`
// Stable plugin/page-key target. When set, these identify the provisioned
// page independently of its current slug.
PagePlugin string `protobuf:"bytes,6,opt,name=page_plugin,json=pagePlugin,proto3" json:"page_plugin,omitempty"`
PageKey string `protobuf:"bytes,7,opt,name=page_key,json=pageKey,proto3" json:"page_key,omitempty"`
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
} }
@ -7022,6 +7065,20 @@ func (x *ProvisionerEnsureMenuItemRequest) GetSortOrder() int32 {
return 0 return 0
} }
func (x *ProvisionerEnsureMenuItemRequest) GetPagePlugin() string {
if x != nil {
return x.PagePlugin
}
return ""
}
func (x *ProvisionerEnsureMenuItemRequest) GetPageKey() string {
if x != nil {
return x.PageKey
}
return ""
}
type ProvisionerEnsureMenuItemResponse struct { type ProvisionerEnsureMenuItemResponse struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
@ -8786,7 +8843,7 @@ const file_v1_capability_proto_rawDesc = "" +
"\x1fProvisionerEnsureSettingRequest\x12\x10\n" + "\x1fProvisionerEnsureSettingRequest\x12\x10\n" +
"\x03key\x18\x01 \x01(\tR\x03key\x12,\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12,\n" +
"\x12default_value_json\x18\x02 \x01(\fR\x10defaultValueJson\"\"\n" + "\x12default_value_json\x18\x02 \x01(\fR\x10defaultValueJson\"\"\n" +
" ProvisionerEnsureSettingResponse\"\x83\x03\n" + " ProvisionerEnsureSettingResponse\"\x92\x04\n" +
"\bPageSeed\x12\x12\n" + "\bPageSeed\x12\x12\n" +
"\x04slug\x18\x01 \x01(\tR\x04slug\x12\x1f\n" + "\x04slug\x18\x01 \x01(\tR\x04slug\x12\x1f\n" +
"\vparent_slug\x18\x02 \x01(\tR\n" + "\vparent_slug\x18\x02 \x01(\tR\n" +
@ -8799,20 +8856,29 @@ const file_v1_capability_proto_rawDesc = "" +
"\x11detail_slug_field\x18\b \x01(\tR\x0fdetailSlugField\x12)\n" + "\x11detail_slug_field\x18\b \x01(\tR\x0fdetailSlugField\x12)\n" +
"\x10reconcile_blocks\x18\t \x01(\bR\x0freconcileBlocks\x12-\n" + "\x10reconcile_blocks\x18\t \x01(\bR\x0freconcileBlocks\x12-\n" +
"\x12reconcile_template\x18\n" + "\x12reconcile_template\x18\n" +
" \x01(\bR\x11reconcileTemplate\"D\n" + " \x01(\bR\x11reconcileTemplate\x12\x10\n" +
"\x03key\x18\v \x01(\tR\x03key\x12\x1d\n" +
"\n" +
"parent_key\x18\f \x01(\tR\tparentKey\x12\x1d\n" +
"\n" +
"mount_path\x18\r \x01(\tR\tmountPath\x12=\n" +
"\x1bmerge_missing_block_content\x18\x0e \x01(\bR\x18mergeMissingBlockContent\"D\n" +
"\x1cProvisionerEnsurePageRequest\x12$\n" + "\x1cProvisionerEnsurePageRequest\x12$\n" +
"\x04page\x18\x01 \x01(\v2\x10.abi.v1.PageSeedR\x04page\"\x1f\n" + "\x04page\x18\x01 \x01(\v2\x10.abi.v1.PageSeedR\x04page\"\x1f\n" +
"\x1dProvisionerEnsurePageResponse\"O\n" + "\x1dProvisionerEnsurePageResponse\"O\n" +
"&ProvisionerOverrideSiteSettingsRequest\x12%\n" + "&ProvisionerOverrideSiteSettingsRequest\x12%\n" +
"\x0eoverrides_json\x18\x01 \x01(\fR\roverridesJson\")\n" + "\x0eoverrides_json\x18\x01 \x01(\fR\roverridesJson\")\n" +
"'ProvisionerOverrideSiteSettingsResponse\"\xa3\x01\n" + "'ProvisionerOverrideSiteSettingsResponse\"\xdf\x01\n" +
" ProvisionerEnsureMenuItemRequest\x12\x1b\n" + " ProvisionerEnsureMenuItemRequest\x12\x1b\n" +
"\tmenu_name\x18\x01 \x01(\tR\bmenuName\x12\x14\n" + "\tmenu_name\x18\x01 \x01(\tR\bmenuName\x12\x14\n" +
"\x05label\x18\x02 \x01(\tR\x05label\x12\x10\n" + "\x05label\x18\x02 \x01(\tR\x05label\x12\x10\n" +
"\x03url\x18\x03 \x01(\tR\x03url\x12\x1b\n" + "\x03url\x18\x03 \x01(\tR\x03url\x12\x1b\n" +
"\tpage_slug\x18\x04 \x01(\tR\bpageSlug\x12\x1d\n" + "\tpage_slug\x18\x04 \x01(\tR\bpageSlug\x12\x1d\n" +
"\n" + "\n" +
"sort_order\x18\x05 \x01(\x05R\tsortOrder\"#\n" + "sort_order\x18\x05 \x01(\x05R\tsortOrder\x12\x1f\n" +
"\vpage_plugin\x18\x06 \x01(\tR\n" +
"pagePlugin\x12\x19\n" +
"\bpage_key\x18\a \x01(\tR\apageKey\"#\n" +
"!ProvisionerEnsureMenuItemResponse\"\x87\x01\n" + "!ProvisionerEnsureMenuItemResponse\"\x87\x01\n" +
")ProvisionerRegisterEmbeddingConfigRequest\x12\x1b\n" + ")ProvisionerRegisterEmbeddingConfigRequest\x12\x1b\n" +
"\ttable_key\x18\x01 \x01(\tR\btableKey\x12#\n" + "\ttable_key\x18\x01 \x01(\tR\btableKey\x12#\n" +

View File

@ -78,6 +78,9 @@ const (
// Run one directory pin decorator (DirectoryExtensions.PinDecorators): // Run one directory pin decorator (DirectoryExtensions.PinDecorators):
// payload = DirectoryPinDecoratorRequest / DirectoryPinDecoratorResponse. // payload = DirectoryPinDecoratorRequest / DirectoryPinDecoratorResponse.
Hook_HOOK_DIRECTORY_PIN_DECORATOR Hook = 15 Hook_HOOK_DIRECTORY_PIN_DECORATOR Hook = 15
// Run one keyed PluginRegistration.LoadOnce callback:
// payload = LoadOnceRequest / LoadOnceResponse.
Hook_HOOK_LOAD_ONCE Hook = 16
) )
// Enum value maps for Hook. // Enum value maps for Hook.
@ -99,6 +102,7 @@ var (
13: "HOOK_BRIDGE_CALL", 13: "HOOK_BRIDGE_CALL",
14: "HOOK_DIRECTORY_PANEL_SECTION", 14: "HOOK_DIRECTORY_PANEL_SECTION",
15: "HOOK_DIRECTORY_PIN_DECORATOR", 15: "HOOK_DIRECTORY_PIN_DECORATOR",
16: "HOOK_LOAD_ONCE",
} }
Hook_value = map[string]int32{ Hook_value = map[string]int32{
"HOOK_UNSPECIFIED": 0, "HOOK_UNSPECIFIED": 0,
@ -117,6 +121,7 @@ var (
"HOOK_BRIDGE_CALL": 13, "HOOK_BRIDGE_CALL": 13,
"HOOK_DIRECTORY_PANEL_SECTION": 14, "HOOK_DIRECTORY_PANEL_SECTION": 14,
"HOOK_DIRECTORY_PIN_DECORATOR": 15, "HOOK_DIRECTORY_PIN_DECORATOR": 15,
"HOOK_LOAD_ONCE": 16,
} }
) )
@ -627,6 +632,89 @@ func (*LoadResponse) Descriptor() ([]byte, []int) {
return file_v1_invoke_proto_rawDescGZIP(), []int{7} return file_v1_invoke_proto_rawDescGZIP(), []int{7}
} }
// LoadOnceRequest identifies the callback selected by the host from the
// manifest's load_once_keys. The host records the key only after a successful
// response; failed callbacks may be retried on a later load.
type LoadOnceRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *LoadOnceRequest) Reset() {
*x = LoadOnceRequest{}
mi := &file_v1_invoke_proto_msgTypes[8]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *LoadOnceRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*LoadOnceRequest) ProtoMessage() {}
func (x *LoadOnceRequest) ProtoReflect() protoreflect.Message {
mi := &file_v1_invoke_proto_msgTypes[8]
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 LoadOnceRequest.ProtoReflect.Descriptor instead.
func (*LoadOnceRequest) Descriptor() ([]byte, []int) {
return file_v1_invoke_proto_rawDescGZIP(), []int{8}
}
func (x *LoadOnceRequest) GetKey() string {
if x != nil {
return x.Key
}
return ""
}
type LoadOnceResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *LoadOnceResponse) Reset() {
*x = LoadOnceResponse{}
mi := &file_v1_invoke_proto_msgTypes[9]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *LoadOnceResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*LoadOnceResponse) ProtoMessage() {}
func (x *LoadOnceResponse) ProtoReflect() protoreflect.Message {
mi := &file_v1_invoke_proto_msgTypes[9]
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 LoadOnceResponse.ProtoReflect.Descriptor instead.
func (*LoadOnceResponse) Descriptor() ([]byte, []int) {
return file_v1_invoke_proto_rawDescGZIP(), []int{9}
}
type UnloadRequest struct { type UnloadRequest struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
@ -635,7 +723,7 @@ type UnloadRequest struct {
func (x *UnloadRequest) Reset() { func (x *UnloadRequest) Reset() {
*x = UnloadRequest{} *x = UnloadRequest{}
mi := &file_v1_invoke_proto_msgTypes[8] mi := &file_v1_invoke_proto_msgTypes[10]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -647,7 +735,7 @@ func (x *UnloadRequest) String() string {
func (*UnloadRequest) ProtoMessage() {} func (*UnloadRequest) ProtoMessage() {}
func (x *UnloadRequest) ProtoReflect() protoreflect.Message { func (x *UnloadRequest) ProtoReflect() protoreflect.Message {
mi := &file_v1_invoke_proto_msgTypes[8] mi := &file_v1_invoke_proto_msgTypes[10]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -660,7 +748,7 @@ func (x *UnloadRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use UnloadRequest.ProtoReflect.Descriptor instead. // Deprecated: Use UnloadRequest.ProtoReflect.Descriptor instead.
func (*UnloadRequest) Descriptor() ([]byte, []int) { func (*UnloadRequest) Descriptor() ([]byte, []int) {
return file_v1_invoke_proto_rawDescGZIP(), []int{8} return file_v1_invoke_proto_rawDescGZIP(), []int{10}
} }
type UnloadResponse struct { type UnloadResponse struct {
@ -671,7 +759,7 @@ type UnloadResponse struct {
func (x *UnloadResponse) Reset() { func (x *UnloadResponse) Reset() {
*x = UnloadResponse{} *x = UnloadResponse{}
mi := &file_v1_invoke_proto_msgTypes[9] mi := &file_v1_invoke_proto_msgTypes[11]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -683,7 +771,7 @@ func (x *UnloadResponse) String() string {
func (*UnloadResponse) ProtoMessage() {} func (*UnloadResponse) ProtoMessage() {}
func (x *UnloadResponse) ProtoReflect() protoreflect.Message { func (x *UnloadResponse) ProtoReflect() protoreflect.Message {
mi := &file_v1_invoke_proto_msgTypes[9] mi := &file_v1_invoke_proto_msgTypes[11]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -696,7 +784,7 @@ func (x *UnloadResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use UnloadResponse.ProtoReflect.Descriptor instead. // Deprecated: Use UnloadResponse.ProtoReflect.Descriptor instead.
func (*UnloadResponse) Descriptor() ([]byte, []int) { func (*UnloadResponse) Descriptor() ([]byte, []int) {
return file_v1_invoke_proto_rawDescGZIP(), []int{9} return file_v1_invoke_proto_rawDescGZIP(), []int{11}
} }
// JobRequest dispatches one background job to the guest handler registered // JobRequest dispatches one background job to the guest handler registered
@ -712,7 +800,7 @@ type JobRequest struct {
func (x *JobRequest) Reset() { func (x *JobRequest) Reset() {
*x = JobRequest{} *x = JobRequest{}
mi := &file_v1_invoke_proto_msgTypes[10] mi := &file_v1_invoke_proto_msgTypes[12]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -724,7 +812,7 @@ func (x *JobRequest) String() string {
func (*JobRequest) ProtoMessage() {} func (*JobRequest) ProtoMessage() {}
func (x *JobRequest) ProtoReflect() protoreflect.Message { func (x *JobRequest) ProtoReflect() protoreflect.Message {
mi := &file_v1_invoke_proto_msgTypes[10] mi := &file_v1_invoke_proto_msgTypes[12]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -737,7 +825,7 @@ func (x *JobRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use JobRequest.ProtoReflect.Descriptor instead. // Deprecated: Use JobRequest.ProtoReflect.Descriptor instead.
func (*JobRequest) Descriptor() ([]byte, []int) { func (*JobRequest) Descriptor() ([]byte, []int) {
return file_v1_invoke_proto_rawDescGZIP(), []int{10} return file_v1_invoke_proto_rawDescGZIP(), []int{12}
} }
func (x *JobRequest) GetJobType() string { func (x *JobRequest) GetJobType() string {
@ -764,7 +852,7 @@ type JobResponse struct {
func (x *JobResponse) Reset() { func (x *JobResponse) Reset() {
*x = JobResponse{} *x = JobResponse{}
mi := &file_v1_invoke_proto_msgTypes[11] mi := &file_v1_invoke_proto_msgTypes[13]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -776,7 +864,7 @@ func (x *JobResponse) String() string {
func (*JobResponse) ProtoMessage() {} func (*JobResponse) ProtoMessage() {}
func (x *JobResponse) ProtoReflect() protoreflect.Message { func (x *JobResponse) ProtoReflect() protoreflect.Message {
mi := &file_v1_invoke_proto_msgTypes[11] mi := &file_v1_invoke_proto_msgTypes[13]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -789,7 +877,7 @@ func (x *JobResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use JobResponse.ProtoReflect.Descriptor instead. // Deprecated: Use JobResponse.ProtoReflect.Descriptor instead.
func (*JobResponse) Descriptor() ([]byte, []int) { func (*JobResponse) Descriptor() ([]byte, []int) {
return file_v1_invoke_proto_rawDescGZIP(), []int{11} return file_v1_invoke_proto_rawDescGZIP(), []int{13}
} }
func (x *JobResponse) GetResultJson() []byte { func (x *JobResponse) GetResultJson() []byte {
@ -811,7 +899,7 @@ type RagFetchRequest struct {
func (x *RagFetchRequest) Reset() { func (x *RagFetchRequest) Reset() {
*x = RagFetchRequest{} *x = RagFetchRequest{}
mi := &file_v1_invoke_proto_msgTypes[12] mi := &file_v1_invoke_proto_msgTypes[14]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -823,7 +911,7 @@ func (x *RagFetchRequest) String() string {
func (*RagFetchRequest) ProtoMessage() {} func (*RagFetchRequest) ProtoMessage() {}
func (x *RagFetchRequest) ProtoReflect() protoreflect.Message { func (x *RagFetchRequest) ProtoReflect() protoreflect.Message {
mi := &file_v1_invoke_proto_msgTypes[12] mi := &file_v1_invoke_proto_msgTypes[14]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -836,7 +924,7 @@ func (x *RagFetchRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use RagFetchRequest.ProtoReflect.Descriptor instead. // Deprecated: Use RagFetchRequest.ProtoReflect.Descriptor instead.
func (*RagFetchRequest) Descriptor() ([]byte, []int) { func (*RagFetchRequest) Descriptor() ([]byte, []int) {
return file_v1_invoke_proto_rawDescGZIP(), []int{12} return file_v1_invoke_proto_rawDescGZIP(), []int{14}
} }
func (x *RagFetchRequest) GetContentType() string { func (x *RagFetchRequest) GetContentType() string {
@ -864,7 +952,7 @@ type RagFetchResponse struct {
func (x *RagFetchResponse) Reset() { func (x *RagFetchResponse) Reset() {
*x = RagFetchResponse{} *x = RagFetchResponse{}
mi := &file_v1_invoke_proto_msgTypes[13] mi := &file_v1_invoke_proto_msgTypes[15]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -876,7 +964,7 @@ func (x *RagFetchResponse) String() string {
func (*RagFetchResponse) ProtoMessage() {} func (*RagFetchResponse) ProtoMessage() {}
func (x *RagFetchResponse) ProtoReflect() protoreflect.Message { func (x *RagFetchResponse) ProtoReflect() protoreflect.Message {
mi := &file_v1_invoke_proto_msgTypes[13] mi := &file_v1_invoke_proto_msgTypes[15]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -889,7 +977,7 @@ func (x *RagFetchResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use RagFetchResponse.ProtoReflect.Descriptor instead. // Deprecated: Use RagFetchResponse.ProtoReflect.Descriptor instead.
func (*RagFetchResponse) Descriptor() ([]byte, []int) { func (*RagFetchResponse) Descriptor() ([]byte, []int) {
return file_v1_invoke_proto_rawDescGZIP(), []int{13} return file_v1_invoke_proto_rawDescGZIP(), []int{15}
} }
func (x *RagFetchResponse) GetTitle() string { func (x *RagFetchResponse) GetTitle() string {
@ -921,7 +1009,7 @@ type MediaHookRequest struct {
func (x *MediaHookRequest) Reset() { func (x *MediaHookRequest) Reset() {
*x = MediaHookRequest{} *x = MediaHookRequest{}
mi := &file_v1_invoke_proto_msgTypes[14] mi := &file_v1_invoke_proto_msgTypes[16]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -933,7 +1021,7 @@ func (x *MediaHookRequest) String() string {
func (*MediaHookRequest) ProtoMessage() {} func (*MediaHookRequest) ProtoMessage() {}
func (x *MediaHookRequest) ProtoReflect() protoreflect.Message { func (x *MediaHookRequest) ProtoReflect() protoreflect.Message {
mi := &file_v1_invoke_proto_msgTypes[14] mi := &file_v1_invoke_proto_msgTypes[16]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -946,7 +1034,7 @@ func (x *MediaHookRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use MediaHookRequest.ProtoReflect.Descriptor instead. // Deprecated: Use MediaHookRequest.ProtoReflect.Descriptor instead.
func (*MediaHookRequest) Descriptor() ([]byte, []int) { func (*MediaHookRequest) Descriptor() ([]byte, []int) {
return file_v1_invoke_proto_rawDescGZIP(), []int{14} return file_v1_invoke_proto_rawDescGZIP(), []int{16}
} }
func (x *MediaHookRequest) GetEvent() isMediaHookRequest_Event { func (x *MediaHookRequest) GetEvent() isMediaHookRequest_Event {
@ -998,7 +1086,7 @@ type MediaHookResponse struct {
func (x *MediaHookResponse) Reset() { func (x *MediaHookResponse) Reset() {
*x = MediaHookResponse{} *x = MediaHookResponse{}
mi := &file_v1_invoke_proto_msgTypes[15] mi := &file_v1_invoke_proto_msgTypes[17]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -1010,7 +1098,7 @@ func (x *MediaHookResponse) String() string {
func (*MediaHookResponse) ProtoMessage() {} func (*MediaHookResponse) ProtoMessage() {}
func (x *MediaHookResponse) ProtoReflect() protoreflect.Message { func (x *MediaHookResponse) ProtoReflect() protoreflect.Message {
mi := &file_v1_invoke_proto_msgTypes[15] mi := &file_v1_invoke_proto_msgTypes[17]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -1023,7 +1111,7 @@ func (x *MediaHookResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use MediaHookResponse.ProtoReflect.Descriptor instead. // Deprecated: Use MediaHookResponse.ProtoReflect.Descriptor instead.
func (*MediaHookResponse) Descriptor() ([]byte, []int) { func (*MediaHookResponse) Descriptor() ([]byte, []int) {
return file_v1_invoke_proto_rawDescGZIP(), []int{15} return file_v1_invoke_proto_rawDescGZIP(), []int{17}
} }
// MediaAnalyzedEvent mirrors plugin.MediaAnalyzedEvent (UUIDs as strings). // MediaAnalyzedEvent mirrors plugin.MediaAnalyzedEvent (UUIDs as strings).
@ -1045,7 +1133,7 @@ type MediaAnalyzedEvent struct {
func (x *MediaAnalyzedEvent) Reset() { func (x *MediaAnalyzedEvent) Reset() {
*x = MediaAnalyzedEvent{} *x = MediaAnalyzedEvent{}
mi := &file_v1_invoke_proto_msgTypes[16] mi := &file_v1_invoke_proto_msgTypes[18]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -1057,7 +1145,7 @@ func (x *MediaAnalyzedEvent) String() string {
func (*MediaAnalyzedEvent) ProtoMessage() {} func (*MediaAnalyzedEvent) ProtoMessage() {}
func (x *MediaAnalyzedEvent) ProtoReflect() protoreflect.Message { func (x *MediaAnalyzedEvent) ProtoReflect() protoreflect.Message {
mi := &file_v1_invoke_proto_msgTypes[16] mi := &file_v1_invoke_proto_msgTypes[18]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -1070,7 +1158,7 @@ func (x *MediaAnalyzedEvent) ProtoReflect() protoreflect.Message {
// Deprecated: Use MediaAnalyzedEvent.ProtoReflect.Descriptor instead. // Deprecated: Use MediaAnalyzedEvent.ProtoReflect.Descriptor instead.
func (*MediaAnalyzedEvent) Descriptor() ([]byte, []int) { func (*MediaAnalyzedEvent) Descriptor() ([]byte, []int) {
return file_v1_invoke_proto_rawDescGZIP(), []int{16} return file_v1_invoke_proto_rawDescGZIP(), []int{18}
} }
func (x *MediaAnalyzedEvent) GetMediaId() string { func (x *MediaAnalyzedEvent) GetMediaId() string {
@ -1156,7 +1244,7 @@ type AiToolCallRequest struct {
func (x *AiToolCallRequest) Reset() { func (x *AiToolCallRequest) Reset() {
*x = AiToolCallRequest{} *x = AiToolCallRequest{}
mi := &file_v1_invoke_proto_msgTypes[17] mi := &file_v1_invoke_proto_msgTypes[19]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -1168,7 +1256,7 @@ func (x *AiToolCallRequest) String() string {
func (*AiToolCallRequest) ProtoMessage() {} func (*AiToolCallRequest) ProtoMessage() {}
func (x *AiToolCallRequest) ProtoReflect() protoreflect.Message { func (x *AiToolCallRequest) ProtoReflect() protoreflect.Message {
mi := &file_v1_invoke_proto_msgTypes[17] mi := &file_v1_invoke_proto_msgTypes[19]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -1181,7 +1269,7 @@ func (x *AiToolCallRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use AiToolCallRequest.ProtoReflect.Descriptor instead. // Deprecated: Use AiToolCallRequest.ProtoReflect.Descriptor instead.
func (*AiToolCallRequest) Descriptor() ([]byte, []int) { func (*AiToolCallRequest) Descriptor() ([]byte, []int) {
return file_v1_invoke_proto_rawDescGZIP(), []int{17} return file_v1_invoke_proto_rawDescGZIP(), []int{19}
} }
func (x *AiToolCallRequest) GetSlug() string { func (x *AiToolCallRequest) GetSlug() string {
@ -1211,7 +1299,7 @@ type AiToolCallResponse struct {
func (x *AiToolCallResponse) Reset() { func (x *AiToolCallResponse) Reset() {
*x = AiToolCallResponse{} *x = AiToolCallResponse{}
mi := &file_v1_invoke_proto_msgTypes[18] mi := &file_v1_invoke_proto_msgTypes[20]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -1223,7 +1311,7 @@ func (x *AiToolCallResponse) String() string {
func (*AiToolCallResponse) ProtoMessage() {} func (*AiToolCallResponse) ProtoMessage() {}
func (x *AiToolCallResponse) ProtoReflect() protoreflect.Message { func (x *AiToolCallResponse) ProtoReflect() protoreflect.Message {
mi := &file_v1_invoke_proto_msgTypes[18] mi := &file_v1_invoke_proto_msgTypes[20]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -1236,7 +1324,7 @@ func (x *AiToolCallResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use AiToolCallResponse.ProtoReflect.Descriptor instead. // Deprecated: Use AiToolCallResponse.ProtoReflect.Descriptor instead.
func (*AiToolCallResponse) Descriptor() ([]byte, []int) { func (*AiToolCallResponse) Descriptor() ([]byte, []int) {
return file_v1_invoke_proto_rawDescGZIP(), []int{18} return file_v1_invoke_proto_rawDescGZIP(), []int{20}
} }
func (x *AiToolCallResponse) GetContent() string { func (x *AiToolCallResponse) GetContent() string {
@ -1262,13 +1350,16 @@ type BridgeCallRequest struct {
ServiceName string `protobuf:"bytes,1,opt,name=service_name,json=serviceName,proto3" json:"service_name,omitempty"` 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"` Method string `protobuf:"bytes,2,opt,name=method,proto3" json:"method,omitempty"`
Payload []byte `protobuf:"bytes,3,opt,name=payload,proto3" json:"payload,omitempty"` Payload []byte `protobuf:"bytes,3,opt,name=payload,proto3" json:"payload,omitempty"`
// Host-authenticated identity of the consumer plugin. The host derives this
// from the loaded caller; providers must not trust identity in payload.
CallerPlugin string `protobuf:"bytes,4,opt,name=caller_plugin,json=callerPlugin,proto3" json:"caller_plugin,omitempty"`
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
} }
func (x *BridgeCallRequest) Reset() { func (x *BridgeCallRequest) Reset() {
*x = BridgeCallRequest{} *x = BridgeCallRequest{}
mi := &file_v1_invoke_proto_msgTypes[19] mi := &file_v1_invoke_proto_msgTypes[21]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -1280,7 +1371,7 @@ func (x *BridgeCallRequest) String() string {
func (*BridgeCallRequest) ProtoMessage() {} func (*BridgeCallRequest) ProtoMessage() {}
func (x *BridgeCallRequest) ProtoReflect() protoreflect.Message { func (x *BridgeCallRequest) ProtoReflect() protoreflect.Message {
mi := &file_v1_invoke_proto_msgTypes[19] mi := &file_v1_invoke_proto_msgTypes[21]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -1293,7 +1384,7 @@ func (x *BridgeCallRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use BridgeCallRequest.ProtoReflect.Descriptor instead. // Deprecated: Use BridgeCallRequest.ProtoReflect.Descriptor instead.
func (*BridgeCallRequest) Descriptor() ([]byte, []int) { func (*BridgeCallRequest) Descriptor() ([]byte, []int) {
return file_v1_invoke_proto_rawDescGZIP(), []int{19} return file_v1_invoke_proto_rawDescGZIP(), []int{21}
} }
func (x *BridgeCallRequest) GetServiceName() string { func (x *BridgeCallRequest) GetServiceName() string {
@ -1317,6 +1408,13 @@ func (x *BridgeCallRequest) GetPayload() []byte {
return nil return nil
} }
func (x *BridgeCallRequest) GetCallerPlugin() string {
if x != nil {
return x.CallerPlugin
}
return ""
}
type BridgeCallResponse struct { type BridgeCallResponse struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
Payload []byte `protobuf:"bytes,1,opt,name=payload,proto3" json:"payload,omitempty"` Payload []byte `protobuf:"bytes,1,opt,name=payload,proto3" json:"payload,omitempty"`
@ -1326,7 +1424,7 @@ type BridgeCallResponse struct {
func (x *BridgeCallResponse) Reset() { func (x *BridgeCallResponse) Reset() {
*x = BridgeCallResponse{} *x = BridgeCallResponse{}
mi := &file_v1_invoke_proto_msgTypes[20] mi := &file_v1_invoke_proto_msgTypes[22]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -1338,7 +1436,7 @@ func (x *BridgeCallResponse) String() string {
func (*BridgeCallResponse) ProtoMessage() {} func (*BridgeCallResponse) ProtoMessage() {}
func (x *BridgeCallResponse) ProtoReflect() protoreflect.Message { func (x *BridgeCallResponse) ProtoReflect() protoreflect.Message {
mi := &file_v1_invoke_proto_msgTypes[20] mi := &file_v1_invoke_proto_msgTypes[22]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -1351,7 +1449,7 @@ func (x *BridgeCallResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use BridgeCallResponse.ProtoReflect.Descriptor instead. // Deprecated: Use BridgeCallResponse.ProtoReflect.Descriptor instead.
func (*BridgeCallResponse) Descriptor() ([]byte, []int) { func (*BridgeCallResponse) Descriptor() ([]byte, []int) {
return file_v1_invoke_proto_rawDescGZIP(), []int{20} return file_v1_invoke_proto_rawDescGZIP(), []int{22}
} }
func (x *BridgeCallResponse) GetPayload() []byte { func (x *BridgeCallResponse) GetPayload() []byte {
@ -1375,7 +1473,7 @@ type DirectoryPanelSectionRequest struct {
func (x *DirectoryPanelSectionRequest) Reset() { func (x *DirectoryPanelSectionRequest) Reset() {
*x = DirectoryPanelSectionRequest{} *x = DirectoryPanelSectionRequest{}
mi := &file_v1_invoke_proto_msgTypes[21] mi := &file_v1_invoke_proto_msgTypes[23]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -1387,7 +1485,7 @@ func (x *DirectoryPanelSectionRequest) String() string {
func (*DirectoryPanelSectionRequest) ProtoMessage() {} func (*DirectoryPanelSectionRequest) ProtoMessage() {}
func (x *DirectoryPanelSectionRequest) ProtoReflect() protoreflect.Message { func (x *DirectoryPanelSectionRequest) ProtoReflect() protoreflect.Message {
mi := &file_v1_invoke_proto_msgTypes[21] mi := &file_v1_invoke_proto_msgTypes[23]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -1400,7 +1498,7 @@ func (x *DirectoryPanelSectionRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use DirectoryPanelSectionRequest.ProtoReflect.Descriptor instead. // Deprecated: Use DirectoryPanelSectionRequest.ProtoReflect.Descriptor instead.
func (*DirectoryPanelSectionRequest) Descriptor() ([]byte, []int) { func (*DirectoryPanelSectionRequest) Descriptor() ([]byte, []int) {
return file_v1_invoke_proto_rawDescGZIP(), []int{21} return file_v1_invoke_proto_rawDescGZIP(), []int{23}
} }
func (x *DirectoryPanelSectionRequest) GetIndex() uint32 { func (x *DirectoryPanelSectionRequest) GetIndex() uint32 {
@ -1426,7 +1524,7 @@ type DirectoryPanelSectionResponse struct {
func (x *DirectoryPanelSectionResponse) Reset() { func (x *DirectoryPanelSectionResponse) Reset() {
*x = DirectoryPanelSectionResponse{} *x = DirectoryPanelSectionResponse{}
mi := &file_v1_invoke_proto_msgTypes[22] mi := &file_v1_invoke_proto_msgTypes[24]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -1438,7 +1536,7 @@ func (x *DirectoryPanelSectionResponse) String() string {
func (*DirectoryPanelSectionResponse) ProtoMessage() {} func (*DirectoryPanelSectionResponse) ProtoMessage() {}
func (x *DirectoryPanelSectionResponse) ProtoReflect() protoreflect.Message { func (x *DirectoryPanelSectionResponse) ProtoReflect() protoreflect.Message {
mi := &file_v1_invoke_proto_msgTypes[22] mi := &file_v1_invoke_proto_msgTypes[24]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -1451,7 +1549,7 @@ func (x *DirectoryPanelSectionResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use DirectoryPanelSectionResponse.ProtoReflect.Descriptor instead. // Deprecated: Use DirectoryPanelSectionResponse.ProtoReflect.Descriptor instead.
func (*DirectoryPanelSectionResponse) Descriptor() ([]byte, []int) { func (*DirectoryPanelSectionResponse) Descriptor() ([]byte, []int) {
return file_v1_invoke_proto_rawDescGZIP(), []int{22} return file_v1_invoke_proto_rawDescGZIP(), []int{24}
} }
func (x *DirectoryPanelSectionResponse) GetHtml() string { func (x *DirectoryPanelSectionResponse) GetHtml() string {
@ -1476,7 +1574,7 @@ type DirectoryPinDecoratorRequest struct {
func (x *DirectoryPinDecoratorRequest) Reset() { func (x *DirectoryPinDecoratorRequest) Reset() {
*x = DirectoryPinDecoratorRequest{} *x = DirectoryPinDecoratorRequest{}
mi := &file_v1_invoke_proto_msgTypes[23] mi := &file_v1_invoke_proto_msgTypes[25]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -1488,7 +1586,7 @@ func (x *DirectoryPinDecoratorRequest) String() string {
func (*DirectoryPinDecoratorRequest) ProtoMessage() {} func (*DirectoryPinDecoratorRequest) ProtoMessage() {}
func (x *DirectoryPinDecoratorRequest) ProtoReflect() protoreflect.Message { func (x *DirectoryPinDecoratorRequest) ProtoReflect() protoreflect.Message {
mi := &file_v1_invoke_proto_msgTypes[23] mi := &file_v1_invoke_proto_msgTypes[25]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -1501,7 +1599,7 @@ func (x *DirectoryPinDecoratorRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use DirectoryPinDecoratorRequest.ProtoReflect.Descriptor instead. // Deprecated: Use DirectoryPinDecoratorRequest.ProtoReflect.Descriptor instead.
func (*DirectoryPinDecoratorRequest) Descriptor() ([]byte, []int) { func (*DirectoryPinDecoratorRequest) Descriptor() ([]byte, []int) {
return file_v1_invoke_proto_rawDescGZIP(), []int{23} return file_v1_invoke_proto_rawDescGZIP(), []int{25}
} }
func (x *DirectoryPinDecoratorRequest) GetIndex() uint32 { func (x *DirectoryPinDecoratorRequest) GetIndex() uint32 {
@ -1535,7 +1633,7 @@ type DirectoryPinDecoratorResponse struct {
func (x *DirectoryPinDecoratorResponse) Reset() { func (x *DirectoryPinDecoratorResponse) Reset() {
*x = DirectoryPinDecoratorResponse{} *x = DirectoryPinDecoratorResponse{}
mi := &file_v1_invoke_proto_msgTypes[24] mi := &file_v1_invoke_proto_msgTypes[26]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -1547,7 +1645,7 @@ func (x *DirectoryPinDecoratorResponse) String() string {
func (*DirectoryPinDecoratorResponse) ProtoMessage() {} func (*DirectoryPinDecoratorResponse) ProtoMessage() {}
func (x *DirectoryPinDecoratorResponse) ProtoReflect() protoreflect.Message { func (x *DirectoryPinDecoratorResponse) ProtoReflect() protoreflect.Message {
mi := &file_v1_invoke_proto_msgTypes[24] mi := &file_v1_invoke_proto_msgTypes[26]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -1560,7 +1658,7 @@ func (x *DirectoryPinDecoratorResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use DirectoryPinDecoratorResponse.ProtoReflect.Descriptor instead. // Deprecated: Use DirectoryPinDecoratorResponse.ProtoReflect.Descriptor instead.
func (*DirectoryPinDecoratorResponse) Descriptor() ([]byte, []int) { func (*DirectoryPinDecoratorResponse) Descriptor() ([]byte, []int) {
return file_v1_invoke_proto_rawDescGZIP(), []int{24} return file_v1_invoke_proto_rawDescGZIP(), []int{26}
} }
func (x *DirectoryPinDecoratorResponse) GetPinJson() []byte { func (x *DirectoryPinDecoratorResponse) GetPinJson() []byte {
@ -1588,7 +1686,7 @@ type ModerationDecisionEvent struct {
func (x *ModerationDecisionEvent) Reset() { func (x *ModerationDecisionEvent) Reset() {
*x = ModerationDecisionEvent{} *x = ModerationDecisionEvent{}
mi := &file_v1_invoke_proto_msgTypes[25] mi := &file_v1_invoke_proto_msgTypes[27]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -1600,7 +1698,7 @@ func (x *ModerationDecisionEvent) String() string {
func (*ModerationDecisionEvent) ProtoMessage() {} func (*ModerationDecisionEvent) ProtoMessage() {}
func (x *ModerationDecisionEvent) ProtoReflect() protoreflect.Message { func (x *ModerationDecisionEvent) ProtoReflect() protoreflect.Message {
mi := &file_v1_invoke_proto_msgTypes[25] mi := &file_v1_invoke_proto_msgTypes[27]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -1613,7 +1711,7 @@ func (x *ModerationDecisionEvent) ProtoReflect() protoreflect.Message {
// Deprecated: Use ModerationDecisionEvent.ProtoReflect.Descriptor instead. // Deprecated: Use ModerationDecisionEvent.ProtoReflect.Descriptor instead.
func (*ModerationDecisionEvent) Descriptor() ([]byte, []int) { func (*ModerationDecisionEvent) Descriptor() ([]byte, []int) {
return file_v1_invoke_proto_rawDescGZIP(), []int{25} return file_v1_invoke_proto_rawDescGZIP(), []int{27}
} }
func (x *ModerationDecisionEvent) GetMediaId() string { func (x *ModerationDecisionEvent) GetMediaId() string {
@ -1707,7 +1805,10 @@ const file_v1_invoke_proto_rawDesc = "" +
"\aapp_url\x18\x01 \x01(\tR\x06appUrl\x12\x1d\n" + "\aapp_url\x18\x01 \x01(\tR\x06appUrl\x12\x1d\n" +
"\n" + "\n" +
"media_path\x18\x02 \x01(\tR\tmediaPath\"\x0e\n" + "media_path\x18\x02 \x01(\tR\tmediaPath\"\x0e\n" +
"\fLoadResponse\"\x0f\n" + "\fLoadResponse\"#\n" +
"\x0fLoadOnceRequest\x12\x10\n" +
"\x03key\x18\x01 \x01(\tR\x03key\"\x12\n" +
"\x10LoadOnceResponse\"\x0f\n" +
"\rUnloadRequest\"\x10\n" + "\rUnloadRequest\"\x10\n" +
"\x0eUnloadResponse\"H\n" + "\x0eUnloadResponse\"H\n" +
"\n" + "\n" +
@ -1751,11 +1852,12 @@ const file_v1_invoke_proto_rawDesc = "" +
"paramsJson\"S\n" + "paramsJson\"S\n" +
"\x12AiToolCallResponse\x12\x18\n" + "\x12AiToolCallResponse\x12\x18\n" +
"\acontent\x18\x01 \x01(\tR\acontent\x12#\n" + "\acontent\x18\x01 \x01(\tR\acontent\x12#\n" +
"\rerror_message\x18\x02 \x01(\tR\ferrorMessage\"h\n" + "\rerror_message\x18\x02 \x01(\tR\ferrorMessage\"\x8d\x01\n" +
"\x11BridgeCallRequest\x12!\n" + "\x11BridgeCallRequest\x12!\n" +
"\fservice_name\x18\x01 \x01(\tR\vserviceName\x12\x16\n" + "\fservice_name\x18\x01 \x01(\tR\vserviceName\x12\x16\n" +
"\x06method\x18\x02 \x01(\tR\x06method\x12\x18\n" + "\x06method\x18\x02 \x01(\tR\x06method\x12\x18\n" +
"\apayload\x18\x03 \x01(\fR\apayload\".\n" + "\apayload\x18\x03 \x01(\fR\apayload\x12#\n" +
"\rcaller_plugin\x18\x04 \x01(\tR\fcallerPlugin\".\n" +
"\x12BridgeCallResponse\x12\x18\n" + "\x12BridgeCallResponse\x12\x18\n" +
"\apayload\x18\x01 \x01(\fR\apayload\"Q\n" + "\apayload\x18\x01 \x01(\fR\apayload\"Q\n" +
"\x1cDirectoryPanelSectionRequest\x12\x14\n" + "\x1cDirectoryPanelSectionRequest\x12\x14\n" +
@ -1780,7 +1882,7 @@ const file_v1_invoke_proto_rawDesc = "" +
"sourceType\x12\"\n" + "sourceType\x12\"\n" +
"\rsource_ref_id\x18\a \x01(\tR\vsourceRefId\x12!\n" + "\rsource_ref_id\x18\a \x01(\tR\vsourceRefId\x12!\n" +
"\fmoderated_by\x18\b \x01(\tR\vmoderatedBy\x12\x12\n" + "\fmoderated_by\x18\b \x01(\tR\vmoderatedBy\x12\x12\n" +
"\x04note\x18\t \x01(\tR\x04note*\xea\x02\n" + "\x04note\x18\t \x01(\tR\x04note*\xfe\x02\n" +
"\x04Hook\x12\x14\n" + "\x04Hook\x12\x14\n" +
"\x10HOOK_UNSPECIFIED\x10\x00\x12\x15\n" + "\x10HOOK_UNSPECIFIED\x10\x00\x12\x15\n" +
"\x11HOOK_RENDER_BLOCK\x10\x01\x12\x18\n" + "\x11HOOK_RENDER_BLOCK\x10\x01\x12\x18\n" +
@ -1798,7 +1900,8 @@ const file_v1_invoke_proto_rawDesc = "" +
"\x11HOOK_AI_TOOL_CALL\x10\f\x12\x14\n" + "\x11HOOK_AI_TOOL_CALL\x10\f\x12\x14\n" +
"\x10HOOK_BRIDGE_CALL\x10\r\x12 \n" + "\x10HOOK_BRIDGE_CALL\x10\r\x12 \n" +
"\x1cHOOK_DIRECTORY_PANEL_SECTION\x10\x0e\x12 \n" + "\x1cHOOK_DIRECTORY_PANEL_SECTION\x10\x0e\x12 \n" +
"\x1cHOOK_DIRECTORY_PIN_DECORATOR\x10\x0f*\x95\x02\n" + "\x1cHOOK_DIRECTORY_PIN_DECORATOR\x10\x0f\x12\x12\n" +
"\x0eHOOK_LOAD_ONCE\x10\x10*\x95\x02\n" +
"\fAbiErrorCode\x12\x1e\n" + "\fAbiErrorCode\x12\x1e\n" +
"\x1aABI_ERROR_CODE_UNSPECIFIED\x10\x00\x12\x1b\n" + "\x1aABI_ERROR_CODE_UNSPECIFIED\x10\x00\x12\x1b\n" +
"\x17ABI_ERROR_CODE_INTERNAL\x10\x01\x12\x19\n" + "\x17ABI_ERROR_CODE_INTERNAL\x10\x01\x12\x19\n" +
@ -1822,7 +1925,7 @@ func file_v1_invoke_proto_rawDescGZIP() []byte {
} }
var file_v1_invoke_proto_enumTypes = make([]protoimpl.EnumInfo, 2) var file_v1_invoke_proto_enumTypes = make([]protoimpl.EnumInfo, 2)
var file_v1_invoke_proto_msgTypes = make([]protoimpl.MessageInfo, 26) var file_v1_invoke_proto_msgTypes = make([]protoimpl.MessageInfo, 28)
var file_v1_invoke_proto_goTypes = []any{ var file_v1_invoke_proto_goTypes = []any{
(Hook)(0), // 0: abi.v1.Hook (Hook)(0), // 0: abi.v1.Hook
(AbiErrorCode)(0), // 1: abi.v1.AbiErrorCode (AbiErrorCode)(0), // 1: abi.v1.AbiErrorCode
@ -1834,34 +1937,36 @@ var file_v1_invoke_proto_goTypes = []any{
(*LoadRequest)(nil), // 7: abi.v1.LoadRequest (*LoadRequest)(nil), // 7: abi.v1.LoadRequest
(*HostConfig)(nil), // 8: abi.v1.HostConfig (*HostConfig)(nil), // 8: abi.v1.HostConfig
(*LoadResponse)(nil), // 9: abi.v1.LoadResponse (*LoadResponse)(nil), // 9: abi.v1.LoadResponse
(*UnloadRequest)(nil), // 10: abi.v1.UnloadRequest (*LoadOnceRequest)(nil), // 10: abi.v1.LoadOnceRequest
(*UnloadResponse)(nil), // 11: abi.v1.UnloadResponse (*LoadOnceResponse)(nil), // 11: abi.v1.LoadOnceResponse
(*JobRequest)(nil), // 12: abi.v1.JobRequest (*UnloadRequest)(nil), // 12: abi.v1.UnloadRequest
(*JobResponse)(nil), // 13: abi.v1.JobResponse (*UnloadResponse)(nil), // 13: abi.v1.UnloadResponse
(*RagFetchRequest)(nil), // 14: abi.v1.RagFetchRequest (*JobRequest)(nil), // 14: abi.v1.JobRequest
(*RagFetchResponse)(nil), // 15: abi.v1.RagFetchResponse (*JobResponse)(nil), // 15: abi.v1.JobResponse
(*MediaHookRequest)(nil), // 16: abi.v1.MediaHookRequest (*RagFetchRequest)(nil), // 16: abi.v1.RagFetchRequest
(*MediaHookResponse)(nil), // 17: abi.v1.MediaHookResponse (*RagFetchResponse)(nil), // 17: abi.v1.RagFetchResponse
(*MediaAnalyzedEvent)(nil), // 18: abi.v1.MediaAnalyzedEvent (*MediaHookRequest)(nil), // 18: abi.v1.MediaHookRequest
(*AiToolCallRequest)(nil), // 19: abi.v1.AiToolCallRequest (*MediaHookResponse)(nil), // 19: abi.v1.MediaHookResponse
(*AiToolCallResponse)(nil), // 20: abi.v1.AiToolCallResponse (*MediaAnalyzedEvent)(nil), // 20: abi.v1.MediaAnalyzedEvent
(*BridgeCallRequest)(nil), // 21: abi.v1.BridgeCallRequest (*AiToolCallRequest)(nil), // 21: abi.v1.AiToolCallRequest
(*BridgeCallResponse)(nil), // 22: abi.v1.BridgeCallResponse (*AiToolCallResponse)(nil), // 22: abi.v1.AiToolCallResponse
(*DirectoryPanelSectionRequest)(nil), // 23: abi.v1.DirectoryPanelSectionRequest (*BridgeCallRequest)(nil), // 23: abi.v1.BridgeCallRequest
(*DirectoryPanelSectionResponse)(nil), // 24: abi.v1.DirectoryPanelSectionResponse (*BridgeCallResponse)(nil), // 24: abi.v1.BridgeCallResponse
(*DirectoryPinDecoratorRequest)(nil), // 25: abi.v1.DirectoryPinDecoratorRequest (*DirectoryPanelSectionRequest)(nil), // 25: abi.v1.DirectoryPanelSectionRequest
(*DirectoryPinDecoratorResponse)(nil), // 26: abi.v1.DirectoryPinDecoratorResponse (*DirectoryPanelSectionResponse)(nil), // 26: abi.v1.DirectoryPanelSectionResponse
(*ModerationDecisionEvent)(nil), // 27: abi.v1.ModerationDecisionEvent (*DirectoryPinDecoratorRequest)(nil), // 27: abi.v1.DirectoryPinDecoratorRequest
(*PluginManifest)(nil), // 28: abi.v1.PluginManifest (*DirectoryPinDecoratorResponse)(nil), // 28: abi.v1.DirectoryPinDecoratorResponse
(*ModerationDecisionEvent)(nil), // 29: abi.v1.ModerationDecisionEvent
(*PluginManifest)(nil), // 30: abi.v1.PluginManifest
} }
var file_v1_invoke_proto_depIdxs = []int32{ var file_v1_invoke_proto_depIdxs = []int32{
0, // 0: abi.v1.InvokeRequest.hook:type_name -> abi.v1.Hook 0, // 0: abi.v1.InvokeRequest.hook:type_name -> abi.v1.Hook
4, // 1: abi.v1.InvokeResponse.error:type_name -> abi.v1.AbiError 4, // 1: abi.v1.InvokeResponse.error:type_name -> abi.v1.AbiError
1, // 2: abi.v1.AbiError.code:type_name -> abi.v1.AbiErrorCode 1, // 2: abi.v1.AbiError.code:type_name -> abi.v1.AbiErrorCode
28, // 3: abi.v1.DescribeResponse.manifest:type_name -> abi.v1.PluginManifest 30, // 3: abi.v1.DescribeResponse.manifest:type_name -> abi.v1.PluginManifest
8, // 4: abi.v1.LoadRequest.host_config:type_name -> abi.v1.HostConfig 8, // 4: abi.v1.LoadRequest.host_config:type_name -> abi.v1.HostConfig
18, // 5: abi.v1.MediaHookRequest.media_analyzed:type_name -> abi.v1.MediaAnalyzedEvent 20, // 5: abi.v1.MediaHookRequest.media_analyzed:type_name -> abi.v1.MediaAnalyzedEvent
27, // 6: abi.v1.MediaHookRequest.moderation_decision:type_name -> abi.v1.ModerationDecisionEvent 29, // 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 output_type
7, // [7:7] is the sub-list for method input_type 7, // [7:7] is the sub-list for method input_type
7, // [7:7] is the sub-list for extension type_name 7, // [7:7] is the sub-list for extension type_name
@ -1875,7 +1980,7 @@ func file_v1_invoke_proto_init() {
return return
} }
file_v1_manifest_proto_init() file_v1_manifest_proto_init()
file_v1_invoke_proto_msgTypes[14].OneofWrappers = []any{ file_v1_invoke_proto_msgTypes[16].OneofWrappers = []any{
(*MediaHookRequest_MediaAnalyzed)(nil), (*MediaHookRequest_MediaAnalyzed)(nil),
(*MediaHookRequest_ModerationDecision)(nil), (*MediaHookRequest_ModerationDecision)(nil),
} }
@ -1885,7 +1990,7 @@ func file_v1_invoke_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(), GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_v1_invoke_proto_rawDesc), len(file_v1_invoke_proto_rawDesc)), RawDescriptor: unsafe.Slice(unsafe.StringData(file_v1_invoke_proto_rawDesc), len(file_v1_invoke_proto_rawDesc)),
NumEnums: 2, NumEnums: 2,
NumMessages: 26, NumMessages: 28,
NumExtensions: 0, NumExtensions: 0,
NumServices: 0, NumServices: 0,
}, },

View File

@ -157,6 +157,11 @@ type PluginManifest struct {
// every procedure against rbac_method_roles before advertising or invoking // every procedure against rbac_method_roles before advertising or invoking
// it, so descriptor metadata cannot widen the plugin's authorization map. // it, so descriptor metadata cannot widen the plugin's authorization map.
McpTools []*MCPToolDescriptor `protobuf:"bytes,38,rep,name=mcp_tools,json=mcpTools,proto3" json:"mcp_tools,omitempty"` McpTools []*MCPToolDescriptor `protobuf:"bytes,38,rep,name=mcp_tools,json=mcpTools,proto3" json:"mcp_tools,omitempty"`
// Stable keys for PluginRegistration.LoadOnce callbacks. The host persists
// successful (plugin,key) executions and invokes HOOK_LOAD_ONCE only for
// keys it has never completed. Changing a key is the explicit migration
// mechanism for running a replacement callback once.
LoadOnceKeys []string `protobuf:"bytes,39,rep,name=load_once_keys,json=loadOnceKeys,proto3" json:"load_once_keys,omitempty"`
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
} }
@ -457,6 +462,13 @@ func (x *PluginManifest) GetMcpTools() []*MCPToolDescriptor {
return nil return nil
} }
func (x *PluginManifest) GetLoadOnceKeys() []string {
if x != nil {
return x.LoadOnceKeys
}
return nil
}
// MCPToolDescriptor is one unary ConnectRPC method prepared for the host MCP // MCPToolDescriptor is one unary ConnectRPC method prepared for the host MCP
// catalogue. Procedure, service, method, documentation, and the request JSON // catalogue. Procedure, service, method, documentation, and the request JSON
// schema are all derived from protobuf reflection during DESCRIBE. // schema are all derived from protobuf reflection during DESCRIBE.
@ -1524,7 +1536,7 @@ var File_v1_manifest_proto protoreflect.FileDescriptor
const file_v1_manifest_proto_rawDesc = "" + const file_v1_manifest_proto_rawDesc = "" +
"\n" + "\n" +
"\x11v1/manifest.proto\x12\x06abi.v1\"\xe7\x0e\n" + "\x11v1/manifest.proto\x12\x06abi.v1\"\x8d\x0f\n" +
"\x0ePluginManifest\x12\x1f\n" + "\x0ePluginManifest\x12\x1f\n" +
"\vabi_version\x18\x01 \x01(\rR\n" + "\vabi_version\x18\x01 \x01(\rR\n" +
"abiVersion\x12\x12\n" + "abiVersion\x12\x12\n" +
@ -1567,7 +1579,8 @@ const file_v1_manifest_proto_rawDesc = "" +
"\x0fmax_response_mb\x18# \x01(\rR\rmaxResponseMb\x128\n" + "\x0fmax_response_mb\x18# \x01(\rR\rmaxResponseMb\x128\n" +
"\rpublic_routes\x18$ \x03(\v2\x13.abi.v1.PublicRouteR\fpublicRoutes\x12\x18\n" + "\rpublic_routes\x18$ \x03(\v2\x13.abi.v1.PublicRouteR\fpublicRoutes\x12\x18\n" +
"\asitemap\x18% \x01(\bR\asitemap\x126\n" + "\asitemap\x18% \x01(\bR\asitemap\x126\n" +
"\tmcp_tools\x18& \x03(\v2\x19.abi.v1.MCPToolDescriptorR\bmcpTools\x1aB\n" + "\tmcp_tools\x18& \x03(\v2\x19.abi.v1.MCPToolDescriptorR\bmcpTools\x12$\n" +
"\x0eload_once_keys\x18' \x03(\tR\floadOnceKeys\x1aB\n" +
"\x14RbacMethodRolesEntry\x12\x10\n" + "\x14RbacMethodRolesEntry\x12\x10\n" +
"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xf2\x01\n" + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xf2\x01\n" +

View File

@ -162,6 +162,13 @@ type PoweredBlock struct {
Template string `protobuf:"bytes,1,opt,name=template,proto3" json:"template,omitempty"` Template string `protobuf:"bytes,1,opt,name=template,proto3" json:"template,omitempty"`
// JSON encoding of the template data map (map[string]any). // JSON encoding of the template data map (map[string]any).
DataJson []byte `protobuf:"bytes,2,opt,name=data_json,json=dataJson,proto3" json:"data_json,omitempty"` DataJson []byte `protobuf:"bytes,2,opt,name=data_json,json=dataJson,proto3" json:"data_json,omitempty"`
// Requested HTTP response status. Before writing headers, the host accepts
// only 200599, uses the highest status class, and keeps the first result
// encountered within that class.
StatusCode int32 `protobuf:"varint,3,opt,name=status_code,json=statusCode,proto3" json:"status_code,omitempty"`
// Structured document metadata merged by the host into the page head. The
// first non-empty value for each field wins in deterministic render order.
Metadata *DocumentMetadata `protobuf:"bytes,4,opt,name=metadata,proto3" json:"metadata,omitempty"`
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
} }
@ -210,6 +217,98 @@ func (x *PoweredBlock) GetDataJson() []byte {
return nil return nil
} }
func (x *PoweredBlock) GetStatusCode() int32 {
if x != nil {
return x.StatusCode
}
return 0
}
func (x *PoweredBlock) GetMetadata() *DocumentMetadata {
if x != nil {
return x.Metadata
}
return nil
}
// DocumentMetadata is the safe, shared subset of document head data a plugin
// may provide. The host validates and escapes every value before rendering.
type DocumentMetadata struct {
state protoimpl.MessageState `protogen:"open.v1"`
Title string `protobuf:"bytes,1,opt,name=title,proto3" json:"title,omitempty"`
Description string `protobuf:"bytes,2,opt,name=description,proto3" json:"description,omitempty"`
CanonicalUrl string `protobuf:"bytes,3,opt,name=canonical_url,json=canonicalUrl,proto3" json:"canonical_url,omitempty"`
ImageUrl string `protobuf:"bytes,4,opt,name=image_url,json=imageUrl,proto3" json:"image_url,omitempty"`
Robots string `protobuf:"bytes,5,opt,name=robots,proto3" json:"robots,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *DocumentMetadata) Reset() {
*x = DocumentMetadata{}
mi := &file_v1_render_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *DocumentMetadata) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*DocumentMetadata) ProtoMessage() {}
func (x *DocumentMetadata) ProtoReflect() protoreflect.Message {
mi := &file_v1_render_proto_msgTypes[3]
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 DocumentMetadata.ProtoReflect.Descriptor instead.
func (*DocumentMetadata) Descriptor() ([]byte, []int) {
return file_v1_render_proto_rawDescGZIP(), []int{3}
}
func (x *DocumentMetadata) GetTitle() string {
if x != nil {
return x.Title
}
return ""
}
func (x *DocumentMetadata) GetDescription() string {
if x != nil {
return x.Description
}
return ""
}
func (x *DocumentMetadata) GetCanonicalUrl() string {
if x != nil {
return x.CanonicalUrl
}
return ""
}
func (x *DocumentMetadata) GetImageUrl() string {
if x != nil {
return x.ImageUrl
}
return ""
}
func (x *DocumentMetadata) GetRobots() string {
if x != nil {
return x.Robots
}
return ""
}
// RenderTemplateRequest renders one template (templates.TemplateFunc). // RenderTemplateRequest renders one template (templates.TemplateFunc).
type RenderTemplateRequest struct { type RenderTemplateRequest struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
@ -223,7 +322,7 @@ type RenderTemplateRequest struct {
func (x *RenderTemplateRequest) Reset() { func (x *RenderTemplateRequest) Reset() {
*x = RenderTemplateRequest{} *x = RenderTemplateRequest{}
mi := &file_v1_render_proto_msgTypes[3] mi := &file_v1_render_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -235,7 +334,7 @@ func (x *RenderTemplateRequest) String() string {
func (*RenderTemplateRequest) ProtoMessage() {} func (*RenderTemplateRequest) ProtoMessage() {}
func (x *RenderTemplateRequest) ProtoReflect() protoreflect.Message { func (x *RenderTemplateRequest) ProtoReflect() protoreflect.Message {
mi := &file_v1_render_proto_msgTypes[3] mi := &file_v1_render_proto_msgTypes[4]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -248,7 +347,7 @@ func (x *RenderTemplateRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use RenderTemplateRequest.ProtoReflect.Descriptor instead. // Deprecated: Use RenderTemplateRequest.ProtoReflect.Descriptor instead.
func (*RenderTemplateRequest) Descriptor() ([]byte, []int) { func (*RenderTemplateRequest) Descriptor() ([]byte, []int) {
return file_v1_render_proto_rawDescGZIP(), []int{3} return file_v1_render_proto_rawDescGZIP(), []int{4}
} }
func (x *RenderTemplateRequest) GetTemplateKey() string { func (x *RenderTemplateRequest) GetTemplateKey() string {
@ -281,7 +380,7 @@ type RenderTemplateResponse struct {
func (x *RenderTemplateResponse) Reset() { func (x *RenderTemplateResponse) Reset() {
*x = RenderTemplateResponse{} *x = RenderTemplateResponse{}
mi := &file_v1_render_proto_msgTypes[4] mi := &file_v1_render_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -293,7 +392,7 @@ func (x *RenderTemplateResponse) String() string {
func (*RenderTemplateResponse) ProtoMessage() {} func (*RenderTemplateResponse) ProtoMessage() {}
func (x *RenderTemplateResponse) ProtoReflect() protoreflect.Message { func (x *RenderTemplateResponse) ProtoReflect() protoreflect.Message {
mi := &file_v1_render_proto_msgTypes[4] mi := &file_v1_render_proto_msgTypes[5]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -306,7 +405,7 @@ func (x *RenderTemplateResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use RenderTemplateResponse.ProtoReflect.Descriptor instead. // Deprecated: Use RenderTemplateResponse.ProtoReflect.Descriptor instead.
func (*RenderTemplateResponse) Descriptor() ([]byte, []int) { func (*RenderTemplateResponse) Descriptor() ([]byte, []int) {
return file_v1_render_proto_rawDescGZIP(), []int{4} return file_v1_render_proto_rawDescGZIP(), []int{5}
} }
func (x *RenderTemplateResponse) GetDocument() *TemplateDocument { func (x *RenderTemplateResponse) GetDocument() *TemplateDocument {
@ -330,13 +429,18 @@ type TemplateDocument struct {
// Extra <html>-element attributes (e.g. data-theme). A "lang" key here is // Extra <html>-element attributes (e.g. data-theme). A "lang" key here is
// ignored — the lang field wins. // ignored — the lang field wins.
HtmlAttrs map[string]string `protobuf:"bytes,6,rep,name=html_attrs,json=htmlAttrs,proto3" json:"html_attrs,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` HtmlAttrs map[string]string `protobuf:"bytes,6,rep,name=html_attrs,json=htmlAttrs,proto3" json:"html_attrs,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
// Requested HTTP response status. The host applies the same validation and
// precedence rules as PoweredBlock.status_code.
StatusCode int32 `protobuf:"varint,7,opt,name=status_code,json=statusCode,proto3" json:"status_code,omitempty"`
// Structured document metadata merged by the host into the page head.
Metadata *DocumentMetadata `protobuf:"bytes,8,opt,name=metadata,proto3" json:"metadata,omitempty"`
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
} }
func (x *TemplateDocument) Reset() { func (x *TemplateDocument) Reset() {
*x = TemplateDocument{} *x = TemplateDocument{}
mi := &file_v1_render_proto_msgTypes[5] mi := &file_v1_render_proto_msgTypes[6]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -348,7 +452,7 @@ func (x *TemplateDocument) String() string {
func (*TemplateDocument) ProtoMessage() {} func (*TemplateDocument) ProtoMessage() {}
func (x *TemplateDocument) ProtoReflect() protoreflect.Message { func (x *TemplateDocument) ProtoReflect() protoreflect.Message {
mi := &file_v1_render_proto_msgTypes[5] mi := &file_v1_render_proto_msgTypes[6]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -361,7 +465,7 @@ func (x *TemplateDocument) ProtoReflect() protoreflect.Message {
// Deprecated: Use TemplateDocument.ProtoReflect.Descriptor instead. // Deprecated: Use TemplateDocument.ProtoReflect.Descriptor instead.
func (*TemplateDocument) Descriptor() ([]byte, []int) { func (*TemplateDocument) Descriptor() ([]byte, []int) {
return file_v1_render_proto_rawDescGZIP(), []int{5} return file_v1_render_proto_rawDescGZIP(), []int{6}
} }
func (x *TemplateDocument) GetBodyHtml() []byte { func (x *TemplateDocument) GetBodyHtml() []byte {
@ -406,6 +510,20 @@ func (x *TemplateDocument) GetHtmlAttrs() map[string]string {
return nil return nil
} }
func (x *TemplateDocument) GetStatusCode() int32 {
if x != nil {
return x.StatusCode
}
return 0
}
func (x *TemplateDocument) GetMetadata() *DocumentMetadata {
if x != nil {
return x.Metadata
}
return nil
}
// RenderTagRequest invokes a plugin-declared template tag (HOOK_RENDER_TAG). // RenderTagRequest invokes a plugin-declared template tag (HOOK_RENDER_TAG).
// The host wires one pongo2 tag per manifest.declared_tags name; when its // The host wires one pongo2 tag per manifest.declared_tags name; when its
// engine hits that tag while rendering a powered block, it calls back here. // engine hits that tag while rendering a powered block, it calls back here.
@ -424,7 +542,7 @@ type RenderTagRequest struct {
func (x *RenderTagRequest) Reset() { func (x *RenderTagRequest) Reset() {
*x = RenderTagRequest{} *x = RenderTagRequest{}
mi := &file_v1_render_proto_msgTypes[6] mi := &file_v1_render_proto_msgTypes[7]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -436,7 +554,7 @@ func (x *RenderTagRequest) String() string {
func (*RenderTagRequest) ProtoMessage() {} func (*RenderTagRequest) ProtoMessage() {}
func (x *RenderTagRequest) ProtoReflect() protoreflect.Message { func (x *RenderTagRequest) ProtoReflect() protoreflect.Message {
mi := &file_v1_render_proto_msgTypes[6] mi := &file_v1_render_proto_msgTypes[7]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -449,7 +567,7 @@ func (x *RenderTagRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use RenderTagRequest.ProtoReflect.Descriptor instead. // Deprecated: Use RenderTagRequest.ProtoReflect.Descriptor instead.
func (*RenderTagRequest) Descriptor() ([]byte, []int) { func (*RenderTagRequest) Descriptor() ([]byte, []int) {
return file_v1_render_proto_rawDescGZIP(), []int{6} return file_v1_render_proto_rawDescGZIP(), []int{7}
} }
func (x *RenderTagRequest) GetTagName() string { func (x *RenderTagRequest) GetTagName() string {
@ -486,7 +604,7 @@ type RenderTagResponse struct {
func (x *RenderTagResponse) Reset() { func (x *RenderTagResponse) Reset() {
*x = RenderTagResponse{} *x = RenderTagResponse{}
mi := &file_v1_render_proto_msgTypes[7] mi := &file_v1_render_proto_msgTypes[8]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -498,7 +616,7 @@ func (x *RenderTagResponse) String() string {
func (*RenderTagResponse) ProtoMessage() {} func (*RenderTagResponse) ProtoMessage() {}
func (x *RenderTagResponse) ProtoReflect() protoreflect.Message { func (x *RenderTagResponse) ProtoReflect() protoreflect.Message {
mi := &file_v1_render_proto_msgTypes[7] mi := &file_v1_render_proto_msgTypes[8]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -511,7 +629,7 @@ func (x *RenderTagResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use RenderTagResponse.ProtoReflect.Descriptor instead. // Deprecated: Use RenderTagResponse.ProtoReflect.Descriptor instead.
func (*RenderTagResponse) Descriptor() ([]byte, []int) { func (*RenderTagResponse) Descriptor() ([]byte, []int) {
return file_v1_render_proto_rawDescGZIP(), []int{7} return file_v1_render_proto_rawDescGZIP(), []int{8}
} }
func (x *RenderTagResponse) GetHtml() string { func (x *RenderTagResponse) GetHtml() string {
@ -544,7 +662,7 @@ type ApplyFilterRequest struct {
func (x *ApplyFilterRequest) Reset() { func (x *ApplyFilterRequest) Reset() {
*x = ApplyFilterRequest{} *x = ApplyFilterRequest{}
mi := &file_v1_render_proto_msgTypes[8] mi := &file_v1_render_proto_msgTypes[9]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -556,7 +674,7 @@ func (x *ApplyFilterRequest) String() string {
func (*ApplyFilterRequest) ProtoMessage() {} func (*ApplyFilterRequest) ProtoMessage() {}
func (x *ApplyFilterRequest) ProtoReflect() protoreflect.Message { func (x *ApplyFilterRequest) ProtoReflect() protoreflect.Message {
mi := &file_v1_render_proto_msgTypes[8] mi := &file_v1_render_proto_msgTypes[9]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -569,7 +687,7 @@ func (x *ApplyFilterRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use ApplyFilterRequest.ProtoReflect.Descriptor instead. // Deprecated: Use ApplyFilterRequest.ProtoReflect.Descriptor instead.
func (*ApplyFilterRequest) Descriptor() ([]byte, []int) { func (*ApplyFilterRequest) Descriptor() ([]byte, []int) {
return file_v1_render_proto_rawDescGZIP(), []int{8} return file_v1_render_proto_rawDescGZIP(), []int{9}
} }
func (x *ApplyFilterRequest) GetFilterName() string { func (x *ApplyFilterRequest) GetFilterName() string {
@ -605,7 +723,7 @@ type ApplyFilterResponse struct {
func (x *ApplyFilterResponse) Reset() { func (x *ApplyFilterResponse) Reset() {
*x = ApplyFilterResponse{} *x = ApplyFilterResponse{}
mi := &file_v1_render_proto_msgTypes[9] mi := &file_v1_render_proto_msgTypes[10]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -617,7 +735,7 @@ func (x *ApplyFilterResponse) String() string {
func (*ApplyFilterResponse) ProtoMessage() {} func (*ApplyFilterResponse) ProtoMessage() {}
func (x *ApplyFilterResponse) ProtoReflect() protoreflect.Message { func (x *ApplyFilterResponse) ProtoReflect() protoreflect.Message {
mi := &file_v1_render_proto_msgTypes[9] mi := &file_v1_render_proto_msgTypes[10]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -630,7 +748,7 @@ func (x *ApplyFilterResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use ApplyFilterResponse.ProtoReflect.Descriptor instead. // Deprecated: Use ApplyFilterResponse.ProtoReflect.Descriptor instead.
func (*ApplyFilterResponse) Descriptor() ([]byte, []int) { func (*ApplyFilterResponse) Descriptor() ([]byte, []int) {
return file_v1_render_proto_rawDescGZIP(), []int{9} return file_v1_render_proto_rawDescGZIP(), []int{10}
} }
func (x *ApplyFilterResponse) GetOutput() string { func (x *ApplyFilterResponse) GetOutput() string {
@ -694,7 +812,7 @@ type RenderContext struct {
func (x *RenderContext) Reset() { func (x *RenderContext) Reset() {
*x = RenderContext{} *x = RenderContext{}
mi := &file_v1_render_proto_msgTypes[10] mi := &file_v1_render_proto_msgTypes[11]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -706,7 +824,7 @@ func (x *RenderContext) String() string {
func (*RenderContext) ProtoMessage() {} func (*RenderContext) ProtoMessage() {}
func (x *RenderContext) ProtoReflect() protoreflect.Message { func (x *RenderContext) ProtoReflect() protoreflect.Message {
mi := &file_v1_render_proto_msgTypes[10] mi := &file_v1_render_proto_msgTypes[11]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -719,7 +837,7 @@ func (x *RenderContext) ProtoReflect() protoreflect.Message {
// Deprecated: Use RenderContext.ProtoReflect.Descriptor instead. // Deprecated: Use RenderContext.ProtoReflect.Descriptor instead.
func (*RenderContext) Descriptor() ([]byte, []int) { func (*RenderContext) Descriptor() ([]byte, []int) {
return file_v1_render_proto_rawDescGZIP(), []int{10} return file_v1_render_proto_rawDescGZIP(), []int{11}
} }
func (x *RenderContext) GetRequest() *RequestInfo { func (x *RenderContext) GetRequest() *RequestInfo {
@ -868,7 +986,7 @@ type RequestInfo struct {
func (x *RequestInfo) Reset() { func (x *RequestInfo) Reset() {
*x = RequestInfo{} *x = RequestInfo{}
mi := &file_v1_render_proto_msgTypes[11] mi := &file_v1_render_proto_msgTypes[12]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -880,7 +998,7 @@ func (x *RequestInfo) String() string {
func (*RequestInfo) ProtoMessage() {} func (*RequestInfo) ProtoMessage() {}
func (x *RequestInfo) ProtoReflect() protoreflect.Message { func (x *RequestInfo) ProtoReflect() protoreflect.Message {
mi := &file_v1_render_proto_msgTypes[11] mi := &file_v1_render_proto_msgTypes[12]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -893,7 +1011,7 @@ func (x *RequestInfo) ProtoReflect() protoreflect.Message {
// Deprecated: Use RequestInfo.ProtoReflect.Descriptor instead. // Deprecated: Use RequestInfo.ProtoReflect.Descriptor instead.
func (*RequestInfo) Descriptor() ([]byte, []int) { func (*RequestInfo) Descriptor() ([]byte, []int) {
return file_v1_render_proto_rawDescGZIP(), []int{11} return file_v1_render_proto_rawDescGZIP(), []int{12}
} }
func (x *RequestInfo) GetMethod() string { func (x *RequestInfo) GetMethod() string {
@ -974,13 +1092,17 @@ type PageContext struct {
Title string `protobuf:"bytes,3,opt,name=title,proto3" json:"title,omitempty"` Title string `protobuf:"bytes,3,opt,name=title,proto3" json:"title,omitempty"`
PostType string `protobuf:"bytes,4,opt,name=post_type,json=postType,proto3" json:"post_type,omitempty"` // "page", "post", "master", "system" PostType string `protobuf:"bytes,4,opt,name=post_type,json=postType,proto3" json:"post_type,omitempty"` // "page", "post", "master", "system"
Status string `protobuf:"bytes,5,opt,name=status,proto3" json:"status,omitempty"` // "published", "draft", "scheduled" Status string `protobuf:"bytes,5,opt,name=status,proto3" json:"status,omitempty"` // "published", "draft", "scheduled"
// Slug derived from the resolved page hierarchy.
DerivedSlug string `protobuf:"bytes,6,opt,name=derived_slug,json=derivedSlug,proto3" json:"derived_slug,omitempty"`
// Effective mount path after host inheritance and normalization.
MountPath string `protobuf:"bytes,7,opt,name=mount_path,json=mountPath,proto3" json:"mount_path,omitempty"`
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
} }
func (x *PageContext) Reset() { func (x *PageContext) Reset() {
*x = PageContext{} *x = PageContext{}
mi := &file_v1_render_proto_msgTypes[12] mi := &file_v1_render_proto_msgTypes[13]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -992,7 +1114,7 @@ func (x *PageContext) String() string {
func (*PageContext) ProtoMessage() {} func (*PageContext) ProtoMessage() {}
func (x *PageContext) ProtoReflect() protoreflect.Message { func (x *PageContext) ProtoReflect() protoreflect.Message {
mi := &file_v1_render_proto_msgTypes[12] mi := &file_v1_render_proto_msgTypes[13]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -1005,7 +1127,7 @@ func (x *PageContext) ProtoReflect() protoreflect.Message {
// Deprecated: Use PageContext.ProtoReflect.Descriptor instead. // Deprecated: Use PageContext.ProtoReflect.Descriptor instead.
func (*PageContext) Descriptor() ([]byte, []int) { func (*PageContext) Descriptor() ([]byte, []int) {
return file_v1_render_proto_rawDescGZIP(), []int{12} return file_v1_render_proto_rawDescGZIP(), []int{13}
} }
func (x *PageContext) GetId() string { func (x *PageContext) GetId() string {
@ -1043,6 +1165,20 @@ func (x *PageContext) GetStatus() string {
return "" return ""
} }
func (x *PageContext) GetDerivedSlug() string {
if x != nil {
return x.DerivedSlug
}
return ""
}
func (x *PageContext) GetMountPath() string {
if x != nil {
return x.MountPath
}
return ""
}
// PostContext mirrors blocks.PostContext. // PostContext mirrors blocks.PostContext.
type PostContext struct { type PostContext struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
@ -1061,7 +1197,7 @@ type PostContext struct {
func (x *PostContext) Reset() { func (x *PostContext) Reset() {
*x = PostContext{} *x = PostContext{}
mi := &file_v1_render_proto_msgTypes[13] mi := &file_v1_render_proto_msgTypes[14]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -1073,7 +1209,7 @@ func (x *PostContext) String() string {
func (*PostContext) ProtoMessage() {} func (*PostContext) ProtoMessage() {}
func (x *PostContext) ProtoReflect() protoreflect.Message { func (x *PostContext) ProtoReflect() protoreflect.Message {
mi := &file_v1_render_proto_msgTypes[13] mi := &file_v1_render_proto_msgTypes[14]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -1086,7 +1222,7 @@ func (x *PostContext) ProtoReflect() protoreflect.Message {
// Deprecated: Use PostContext.ProtoReflect.Descriptor instead. // Deprecated: Use PostContext.ProtoReflect.Descriptor instead.
func (*PostContext) Descriptor() ([]byte, []int) { func (*PostContext) Descriptor() ([]byte, []int) {
return file_v1_render_proto_rawDescGZIP(), []int{13} return file_v1_render_proto_rawDescGZIP(), []int{14}
} }
func (x *PostContext) GetId() string { func (x *PostContext) GetId() string {
@ -1166,7 +1302,7 @@ type AuthorContext struct {
func (x *AuthorContext) Reset() { func (x *AuthorContext) Reset() {
*x = AuthorContext{} *x = AuthorContext{}
mi := &file_v1_render_proto_msgTypes[14] mi := &file_v1_render_proto_msgTypes[15]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -1178,7 +1314,7 @@ func (x *AuthorContext) String() string {
func (*AuthorContext) ProtoMessage() {} func (*AuthorContext) ProtoMessage() {}
func (x *AuthorContext) ProtoReflect() protoreflect.Message { func (x *AuthorContext) ProtoReflect() protoreflect.Message {
mi := &file_v1_render_proto_msgTypes[14] mi := &file_v1_render_proto_msgTypes[15]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -1191,7 +1327,7 @@ func (x *AuthorContext) ProtoReflect() protoreflect.Message {
// Deprecated: Use AuthorContext.ProtoReflect.Descriptor instead. // Deprecated: Use AuthorContext.ProtoReflect.Descriptor instead.
func (*AuthorContext) Descriptor() ([]byte, []int) { func (*AuthorContext) Descriptor() ([]byte, []int) {
return file_v1_render_proto_rawDescGZIP(), []int{14} return file_v1_render_proto_rawDescGZIP(), []int{15}
} }
func (x *AuthorContext) GetId() string { func (x *AuthorContext) GetId() string {
@ -1241,7 +1377,7 @@ type CategoryContext struct {
func (x *CategoryContext) Reset() { func (x *CategoryContext) Reset() {
*x = CategoryContext{} *x = CategoryContext{}
mi := &file_v1_render_proto_msgTypes[15] mi := &file_v1_render_proto_msgTypes[16]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -1253,7 +1389,7 @@ func (x *CategoryContext) String() string {
func (*CategoryContext) ProtoMessage() {} func (*CategoryContext) ProtoMessage() {}
func (x *CategoryContext) ProtoReflect() protoreflect.Message { func (x *CategoryContext) ProtoReflect() protoreflect.Message {
mi := &file_v1_render_proto_msgTypes[15] mi := &file_v1_render_proto_msgTypes[16]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -1266,7 +1402,7 @@ func (x *CategoryContext) ProtoReflect() protoreflect.Message {
// Deprecated: Use CategoryContext.ProtoReflect.Descriptor instead. // Deprecated: Use CategoryContext.ProtoReflect.Descriptor instead.
func (*CategoryContext) Descriptor() ([]byte, []int) { func (*CategoryContext) Descriptor() ([]byte, []int) {
return file_v1_render_proto_rawDescGZIP(), []int{15} return file_v1_render_proto_rawDescGZIP(), []int{16}
} }
func (x *CategoryContext) GetId() string { func (x *CategoryContext) GetId() string {
@ -1302,7 +1438,7 @@ type MasterPageContext struct {
func (x *MasterPageContext) Reset() { func (x *MasterPageContext) Reset() {
*x = MasterPageContext{} *x = MasterPageContext{}
mi := &file_v1_render_proto_msgTypes[16] mi := &file_v1_render_proto_msgTypes[17]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -1314,7 +1450,7 @@ func (x *MasterPageContext) String() string {
func (*MasterPageContext) ProtoMessage() {} func (*MasterPageContext) ProtoMessage() {}
func (x *MasterPageContext) ProtoReflect() protoreflect.Message { func (x *MasterPageContext) ProtoReflect() protoreflect.Message {
mi := &file_v1_render_proto_msgTypes[16] mi := &file_v1_render_proto_msgTypes[17]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -1327,7 +1463,7 @@ func (x *MasterPageContext) ProtoReflect() protoreflect.Message {
// Deprecated: Use MasterPageContext.ProtoReflect.Descriptor instead. // Deprecated: Use MasterPageContext.ProtoReflect.Descriptor instead.
func (*MasterPageContext) Descriptor() ([]byte, []int) { func (*MasterPageContext) Descriptor() ([]byte, []int) {
return file_v1_render_proto_rawDescGZIP(), []int{16} return file_v1_render_proto_rawDescGZIP(), []int{17}
} }
func (x *MasterPageContext) GetId() string { func (x *MasterPageContext) GetId() string {
@ -1364,7 +1500,7 @@ type HumanProofBanner struct {
func (x *HumanProofBanner) Reset() { func (x *HumanProofBanner) Reset() {
*x = HumanProofBanner{} *x = HumanProofBanner{}
mi := &file_v1_render_proto_msgTypes[17] mi := &file_v1_render_proto_msgTypes[18]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -1376,7 +1512,7 @@ func (x *HumanProofBanner) String() string {
func (*HumanProofBanner) ProtoMessage() {} func (*HumanProofBanner) ProtoMessage() {}
func (x *HumanProofBanner) ProtoReflect() protoreflect.Message { func (x *HumanProofBanner) ProtoReflect() protoreflect.Message {
mi := &file_v1_render_proto_msgTypes[17] mi := &file_v1_render_proto_msgTypes[18]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -1389,7 +1525,7 @@ func (x *HumanProofBanner) ProtoReflect() protoreflect.Message {
// Deprecated: Use HumanProofBanner.ProtoReflect.Descriptor instead. // Deprecated: Use HumanProofBanner.ProtoReflect.Descriptor instead.
func (*HumanProofBanner) Descriptor() ([]byte, []int) { func (*HumanProofBanner) Descriptor() ([]byte, []int) {
return file_v1_render_proto_rawDescGZIP(), []int{17} return file_v1_render_proto_rawDescGZIP(), []int{18}
} }
func (x *HumanProofBanner) GetActiveTimeMinutes() int32 { func (x *HumanProofBanner) GetActiveTimeMinutes() int32 {
@ -1433,7 +1569,7 @@ type DetailRow struct {
func (x *DetailRow) Reset() { func (x *DetailRow) Reset() {
*x = DetailRow{} *x = DetailRow{}
mi := &file_v1_render_proto_msgTypes[18] mi := &file_v1_render_proto_msgTypes[19]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -1445,7 +1581,7 @@ func (x *DetailRow) String() string {
func (*DetailRow) ProtoMessage() {} func (*DetailRow) ProtoMessage() {}
func (x *DetailRow) ProtoReflect() protoreflect.Message { func (x *DetailRow) ProtoReflect() protoreflect.Message {
mi := &file_v1_render_proto_msgTypes[18] mi := &file_v1_render_proto_msgTypes[19]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -1458,7 +1594,7 @@ func (x *DetailRow) ProtoReflect() protoreflect.Message {
// Deprecated: Use DetailRow.ProtoReflect.Descriptor instead. // Deprecated: Use DetailRow.ProtoReflect.Descriptor instead.
func (*DetailRow) Descriptor() ([]byte, []int) { func (*DetailRow) Descriptor() ([]byte, []int) {
return file_v1_render_proto_rawDescGZIP(), []int{18} return file_v1_render_proto_rawDescGZIP(), []int{19}
} }
func (x *DetailRow) GetTableId() string { func (x *DetailRow) GetTableId() string {
@ -1524,13 +1660,17 @@ type BlockContext struct {
DetailRowDataJson []byte `protobuf:"bytes,36,opt,name=detail_row_data_json,json=detailRowDataJson,proto3" json:"detail_row_data_json,omitempty"` DetailRowDataJson []byte `protobuf:"bytes,36,opt,name=detail_row_data_json,json=detailRowDataJson,proto3" json:"detail_row_data_json,omitempty"`
BlogIndexUrl string `protobuf:"bytes,37,opt,name=blog_index_url,json=blogIndexUrl,proto3" json:"blog_index_url,omitempty"` BlogIndexUrl string `protobuf:"bytes,37,opt,name=blog_index_url,json=blogIndexUrl,proto3" json:"blog_index_url,omitempty"`
CategoryPageUrl string `protobuf:"bytes,38,opt,name=category_page_url,json=categoryPageUrl,proto3" json:"category_page_url,omitempty"` CategoryPageUrl string `protobuf:"bytes,38,opt,name=category_page_url,json=categoryPageUrl,proto3" json:"category_page_url,omitempty"`
// Slug derived from the resolved page hierarchy.
DerivedSlug string `protobuf:"bytes,39,opt,name=derived_slug,json=derivedSlug,proto3" json:"derived_slug,omitempty"`
// Effective mount path after host inheritance and normalization.
MountPath string `protobuf:"bytes,40,opt,name=mount_path,json=mountPath,proto3" json:"mount_path,omitempty"`
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
} }
func (x *BlockContext) Reset() { func (x *BlockContext) Reset() {
*x = BlockContext{} *x = BlockContext{}
mi := &file_v1_render_proto_msgTypes[19] mi := &file_v1_render_proto_msgTypes[20]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -1542,7 +1682,7 @@ func (x *BlockContext) String() string {
func (*BlockContext) ProtoMessage() {} func (*BlockContext) ProtoMessage() {}
func (x *BlockContext) ProtoReflect() protoreflect.Message { func (x *BlockContext) ProtoReflect() protoreflect.Message {
mi := &file_v1_render_proto_msgTypes[19] mi := &file_v1_render_proto_msgTypes[20]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -1555,7 +1695,7 @@ func (x *BlockContext) ProtoReflect() protoreflect.Message {
// Deprecated: Use BlockContext.ProtoReflect.Descriptor instead. // Deprecated: Use BlockContext.ProtoReflect.Descriptor instead.
func (*BlockContext) Descriptor() ([]byte, []int) { func (*BlockContext) Descriptor() ([]byte, []int) {
return file_v1_render_proto_rawDescGZIP(), []int{19} return file_v1_render_proto_rawDescGZIP(), []int{20}
} }
func (x *BlockContext) GetUrl() string { func (x *BlockContext) GetUrl() string {
@ -1824,6 +1964,20 @@ func (x *BlockContext) GetCategoryPageUrl() string {
return "" return ""
} }
func (x *BlockContext) GetDerivedSlug() string {
if x != nil {
return x.DerivedSlug
}
return ""
}
func (x *BlockContext) GetMountPath() string {
if x != nil {
return x.MountPath
}
return ""
}
var File_v1_render_proto protoreflect.FileDescriptor var File_v1_render_proto protoreflect.FileDescriptor
const file_v1_render_proto_rawDesc = "" + const file_v1_render_proto_rawDesc = "" +
@ -1835,16 +1989,25 @@ const file_v1_render_proto_rawDesc = "" +
"\x0erender_context\x18\x03 \x01(\v2\x15.abi.v1.RenderContextR\rrenderContext\"Y\n" + "\x0erender_context\x18\x03 \x01(\v2\x15.abi.v1.RenderContextR\rrenderContext\"Y\n" +
"\x13RenderBlockResponse\x12\x12\n" + "\x13RenderBlockResponse\x12\x12\n" +
"\x04html\x18\x01 \x01(\tR\x04html\x12.\n" + "\x04html\x18\x01 \x01(\tR\x04html\x12.\n" +
"\apowered\x18\x02 \x01(\v2\x14.abi.v1.PoweredBlockR\apowered\"G\n" + "\apowered\x18\x02 \x01(\v2\x14.abi.v1.PoweredBlockR\apowered\"\x9e\x01\n" +
"\fPoweredBlock\x12\x1a\n" + "\fPoweredBlock\x12\x1a\n" +
"\btemplate\x18\x01 \x01(\tR\btemplate\x12\x1b\n" + "\btemplate\x18\x01 \x01(\tR\btemplate\x12\x1b\n" +
"\tdata_json\x18\x02 \x01(\fR\bdataJson\"\x93\x01\n" + "\tdata_json\x18\x02 \x01(\fR\bdataJson\x12\x1f\n" +
"\vstatus_code\x18\x03 \x01(\x05R\n" +
"statusCode\x124\n" +
"\bmetadata\x18\x04 \x01(\v2\x18.abi.v1.DocumentMetadataR\bmetadata\"\xa4\x01\n" +
"\x10DocumentMetadata\x12\x14\n" +
"\x05title\x18\x01 \x01(\tR\x05title\x12 \n" +
"\vdescription\x18\x02 \x01(\tR\vdescription\x12#\n" +
"\rcanonical_url\x18\x03 \x01(\tR\fcanonicalUrl\x12\x1b\n" +
"\timage_url\x18\x04 \x01(\tR\bimageUrl\x12\x16\n" +
"\x06robots\x18\x05 \x01(\tR\x06robots\"\x93\x01\n" +
"\x15RenderTemplateRequest\x12!\n" + "\x15RenderTemplateRequest\x12!\n" +
"\ftemplate_key\x18\x01 \x01(\tR\vtemplateKey\x12\x19\n" + "\ftemplate_key\x18\x01 \x01(\tR\vtemplateKey\x12\x19\n" +
"\bdoc_json\x18\x02 \x01(\fR\adocJson\x12<\n" + "\bdoc_json\x18\x02 \x01(\fR\adocJson\x12<\n" +
"\x0erender_context\x18\x03 \x01(\v2\x15.abi.v1.RenderContextR\rrenderContext\"T\n" + "\x0erender_context\x18\x03 \x01(\v2\x15.abi.v1.RenderContextR\rrenderContext\"T\n" +
"\x16RenderTemplateResponse\x124\n" + "\x16RenderTemplateResponse\x124\n" +
"\bdocument\x18\x02 \x01(\v2\x18.abi.v1.TemplateDocumentR\bdocumentJ\x04\b\x01\x10\x02\"\xbf\x02\n" + "\bdocument\x18\x02 \x01(\v2\x18.abi.v1.TemplateDocumentR\bdocumentJ\x04\b\x01\x10\x02\"\x96\x03\n" +
"\x10TemplateDocument\x12\x1b\n" + "\x10TemplateDocument\x12\x1b\n" +
"\tbody_html\x18\x01 \x01(\fR\bbodyHtml\x12\x1d\n" + "\tbody_html\x18\x01 \x01(\fR\bbodyHtml\x12\x1d\n" +
"\n" + "\n" +
@ -1853,7 +2016,10 @@ const file_v1_render_proto_rawDesc = "" +
"\x0fhead_extra_html\x18\x04 \x01(\fR\rheadExtraHtml\x12-\n" + "\x0fhead_extra_html\x18\x04 \x01(\fR\rheadExtraHtml\x12-\n" +
"\x13body_end_extra_html\x18\x05 \x01(\fR\x10bodyEndExtraHtml\x12F\n" + "\x13body_end_extra_html\x18\x05 \x01(\fR\x10bodyEndExtraHtml\x12F\n" +
"\n" + "\n" +
"html_attrs\x18\x06 \x03(\v2'.abi.v1.TemplateDocument.HtmlAttrsEntryR\thtmlAttrs\x1a<\n" + "html_attrs\x18\x06 \x03(\v2'.abi.v1.TemplateDocument.HtmlAttrsEntryR\thtmlAttrs\x12\x1f\n" +
"\vstatus_code\x18\a \x01(\x05R\n" +
"statusCode\x124\n" +
"\bmetadata\x18\b \x01(\v2\x18.abi.v1.DocumentMetadataR\bmetadata\x1a<\n" +
"\x0eHtmlAttrsEntry\x12\x10\n" + "\x0eHtmlAttrsEntry\x12\x10\n" +
"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\x88\x01\n" + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\x88\x01\n" +
@ -1916,13 +2082,16 @@ const file_v1_render_proto_rawDesc = "" +
"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\x1a:\n" + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\x1a:\n" +
"\fCookiesEntry\x12\x10\n" + "\fCookiesEntry\x12\x10\n" +
"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"|\n" + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xbe\x01\n" +
"\vPageContext\x12\x0e\n" + "\vPageContext\x12\x0e\n" +
"\x02id\x18\x01 \x01(\tR\x02id\x12\x12\n" + "\x02id\x18\x01 \x01(\tR\x02id\x12\x12\n" +
"\x04slug\x18\x02 \x01(\tR\x04slug\x12\x14\n" + "\x04slug\x18\x02 \x01(\tR\x04slug\x12\x14\n" +
"\x05title\x18\x03 \x01(\tR\x05title\x12\x1b\n" + "\x05title\x18\x03 \x01(\tR\x05title\x12\x1b\n" +
"\tpost_type\x18\x04 \x01(\tR\bpostType\x12\x16\n" + "\tpost_type\x18\x04 \x01(\tR\bpostType\x12\x16\n" +
"\x06status\x18\x05 \x01(\tR\x06status\"\xaf\x02\n" + "\x06status\x18\x05 \x01(\tR\x06status\x12!\n" +
"\fderived_slug\x18\x06 \x01(\tR\vderivedSlug\x12\x1d\n" +
"\n" +
"mount_path\x18\a \x01(\tR\tmountPath\"\xaf\x02\n" +
"\vPostContext\x12\x0e\n" + "\vPostContext\x12\x0e\n" +
"\x02id\x18\x01 \x01(\tR\x02id\x12\x12\n" + "\x02id\x18\x01 \x01(\tR\x02id\x12\x12\n" +
"\x04slug\x18\x02 \x01(\tR\x04slug\x12\x14\n" + "\x04slug\x18\x02 \x01(\tR\x04slug\x12\x14\n" +
@ -1957,7 +2126,7 @@ const file_v1_render_proto_rawDesc = "" +
"\tDetailRow\x12\x19\n" + "\tDetailRow\x12\x19\n" +
"\btable_id\x18\x01 \x01(\tR\atableId\x12\x15\n" + "\btable_id\x18\x01 \x01(\tR\atableId\x12\x15\n" +
"\x06row_id\x18\x02 \x01(\tR\x05rowId\x12\x1b\n" + "\x06row_id\x18\x02 \x01(\tR\x05rowId\x12\x1b\n" +
"\tdata_json\x18\x03 \x01(\fR\bdataJson\"\x85\f\n" + "\tdata_json\x18\x03 \x01(\fR\bdataJson\"\xc7\f\n" +
"\fBlockContext\x12\x10\n" + "\fBlockContext\x12\x10\n" +
"\x03url\x18\x01 \x01(\tR\x03url\x12\x12\n" + "\x03url\x18\x01 \x01(\tR\x03url\x12\x12\n" +
"\x04path\x18\x02 \x01(\tR\x04path\x12\x12\n" + "\x04path\x18\x02 \x01(\tR\x04path\x12\x12\n" +
@ -2001,7 +2170,10 @@ const file_v1_render_proto_rawDesc = "" +
"\x0fdetail_table_id\x18# \x01(\tR\rdetailTableId\x12/\n" + "\x0fdetail_table_id\x18# \x01(\tR\rdetailTableId\x12/\n" +
"\x14detail_row_data_json\x18$ \x01(\fR\x11detailRowDataJson\x12$\n" + "\x14detail_row_data_json\x18$ \x01(\fR\x11detailRowDataJson\x12$\n" +
"\x0eblog_index_url\x18% \x01(\tR\fblogIndexUrl\x12*\n" + "\x0eblog_index_url\x18% \x01(\tR\fblogIndexUrl\x12*\n" +
"\x11category_page_url\x18& \x01(\tR\x0fcategoryPageUrl\x1a8\n" + "\x11category_page_url\x18& \x01(\tR\x0fcategoryPageUrl\x12!\n" +
"\fderived_slug\x18' \x01(\tR\vderivedSlug\x12\x1d\n" +
"\n" +
"mount_path\x18( \x01(\tR\tmountPath\x1a8\n" +
"\n" + "\n" +
"QueryEntry\x12\x10\n" + "QueryEntry\x12\x10\n" +
"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
@ -2025,66 +2197,69 @@ func file_v1_render_proto_rawDescGZIP() []byte {
return file_v1_render_proto_rawDescData return file_v1_render_proto_rawDescData
} }
var file_v1_render_proto_msgTypes = make([]protoimpl.MessageInfo, 27) var file_v1_render_proto_msgTypes = make([]protoimpl.MessageInfo, 28)
var file_v1_render_proto_goTypes = []any{ var file_v1_render_proto_goTypes = []any{
(*RenderBlockRequest)(nil), // 0: abi.v1.RenderBlockRequest (*RenderBlockRequest)(nil), // 0: abi.v1.RenderBlockRequest
(*RenderBlockResponse)(nil), // 1: abi.v1.RenderBlockResponse (*RenderBlockResponse)(nil), // 1: abi.v1.RenderBlockResponse
(*PoweredBlock)(nil), // 2: abi.v1.PoweredBlock (*PoweredBlock)(nil), // 2: abi.v1.PoweredBlock
(*RenderTemplateRequest)(nil), // 3: abi.v1.RenderTemplateRequest (*DocumentMetadata)(nil), // 3: abi.v1.DocumentMetadata
(*RenderTemplateResponse)(nil), // 4: abi.v1.RenderTemplateResponse (*RenderTemplateRequest)(nil), // 4: abi.v1.RenderTemplateRequest
(*TemplateDocument)(nil), // 5: abi.v1.TemplateDocument (*RenderTemplateResponse)(nil), // 5: abi.v1.RenderTemplateResponse
(*RenderTagRequest)(nil), // 6: abi.v1.RenderTagRequest (*TemplateDocument)(nil), // 6: abi.v1.TemplateDocument
(*RenderTagResponse)(nil), // 7: abi.v1.RenderTagResponse (*RenderTagRequest)(nil), // 7: abi.v1.RenderTagRequest
(*ApplyFilterRequest)(nil), // 8: abi.v1.ApplyFilterRequest (*RenderTagResponse)(nil), // 8: abi.v1.RenderTagResponse
(*ApplyFilterResponse)(nil), // 9: abi.v1.ApplyFilterResponse (*ApplyFilterRequest)(nil), // 9: abi.v1.ApplyFilterRequest
(*RenderContext)(nil), // 10: abi.v1.RenderContext (*ApplyFilterResponse)(nil), // 10: abi.v1.ApplyFilterResponse
(*RequestInfo)(nil), // 11: abi.v1.RequestInfo (*RenderContext)(nil), // 11: abi.v1.RenderContext
(*PageContext)(nil), // 12: abi.v1.PageContext (*RequestInfo)(nil), // 12: abi.v1.RequestInfo
(*PostContext)(nil), // 13: abi.v1.PostContext (*PageContext)(nil), // 13: abi.v1.PageContext
(*AuthorContext)(nil), // 14: abi.v1.AuthorContext (*PostContext)(nil), // 14: abi.v1.PostContext
(*CategoryContext)(nil), // 15: abi.v1.CategoryContext (*AuthorContext)(nil), // 15: abi.v1.AuthorContext
(*MasterPageContext)(nil), // 16: abi.v1.MasterPageContext (*CategoryContext)(nil), // 16: abi.v1.CategoryContext
(*HumanProofBanner)(nil), // 17: abi.v1.HumanProofBanner (*MasterPageContext)(nil), // 17: abi.v1.MasterPageContext
(*DetailRow)(nil), // 18: abi.v1.DetailRow (*HumanProofBanner)(nil), // 18: abi.v1.HumanProofBanner
(*BlockContext)(nil), // 19: abi.v1.BlockContext (*DetailRow)(nil), // 19: abi.v1.DetailRow
nil, // 20: abi.v1.TemplateDocument.HtmlAttrsEntry (*BlockContext)(nil), // 20: abi.v1.BlockContext
nil, // 21: abi.v1.RenderContext.InjectedSlotsEntry nil, // 21: abi.v1.TemplateDocument.HtmlAttrsEntry
nil, // 22: abi.v1.RequestInfo.HeadersEntry nil, // 22: abi.v1.RenderContext.InjectedSlotsEntry
nil, // 23: abi.v1.RequestInfo.CookiesEntry nil, // 23: abi.v1.RequestInfo.HeadersEntry
nil, // 24: abi.v1.BlockContext.QueryEntry nil, // 24: abi.v1.RequestInfo.CookiesEntry
nil, // 25: abi.v1.BlockContext.CookiesEntry nil, // 25: abi.v1.BlockContext.QueryEntry
nil, // 26: abi.v1.BlockContext.HeadersEntry nil, // 26: abi.v1.BlockContext.CookiesEntry
(*timestamppb.Timestamp)(nil), // 27: google.protobuf.Timestamp nil, // 27: abi.v1.BlockContext.HeadersEntry
(*timestamppb.Timestamp)(nil), // 28: google.protobuf.Timestamp
} }
var file_v1_render_proto_depIdxs = []int32{ var file_v1_render_proto_depIdxs = []int32{
10, // 0: abi.v1.RenderBlockRequest.render_context:type_name -> abi.v1.RenderContext 11, // 0: abi.v1.RenderBlockRequest.render_context:type_name -> abi.v1.RenderContext
2, // 1: abi.v1.RenderBlockResponse.powered:type_name -> abi.v1.PoweredBlock 2, // 1: abi.v1.RenderBlockResponse.powered:type_name -> abi.v1.PoweredBlock
10, // 2: abi.v1.RenderTemplateRequest.render_context:type_name -> abi.v1.RenderContext 3, // 2: abi.v1.PoweredBlock.metadata:type_name -> abi.v1.DocumentMetadata
5, // 3: abi.v1.RenderTemplateResponse.document:type_name -> abi.v1.TemplateDocument 11, // 3: abi.v1.RenderTemplateRequest.render_context:type_name -> abi.v1.RenderContext
20, // 4: abi.v1.TemplateDocument.html_attrs:type_name -> abi.v1.TemplateDocument.HtmlAttrsEntry 6, // 4: abi.v1.RenderTemplateResponse.document:type_name -> abi.v1.TemplateDocument
10, // 5: abi.v1.RenderTagRequest.render_context:type_name -> abi.v1.RenderContext 21, // 5: abi.v1.TemplateDocument.html_attrs:type_name -> abi.v1.TemplateDocument.HtmlAttrsEntry
11, // 6: abi.v1.RenderContext.request:type_name -> abi.v1.RequestInfo 3, // 6: abi.v1.TemplateDocument.metadata:type_name -> abi.v1.DocumentMetadata
19, // 7: abi.v1.RenderContext.block_context:type_name -> abi.v1.BlockContext 11, // 7: abi.v1.RenderTagRequest.render_context:type_name -> abi.v1.RenderContext
12, // 8: abi.v1.RenderContext.page:type_name -> abi.v1.PageContext 12, // 8: abi.v1.RenderContext.request:type_name -> abi.v1.RequestInfo
13, // 9: abi.v1.RenderContext.post:type_name -> abi.v1.PostContext 20, // 9: abi.v1.RenderContext.block_context:type_name -> abi.v1.BlockContext
14, // 10: abi.v1.RenderContext.author:type_name -> abi.v1.AuthorContext 13, // 10: abi.v1.RenderContext.page:type_name -> abi.v1.PageContext
15, // 11: abi.v1.RenderContext.category:type_name -> abi.v1.CategoryContext 14, // 11: abi.v1.RenderContext.post:type_name -> abi.v1.PostContext
16, // 12: abi.v1.RenderContext.master_page:type_name -> abi.v1.MasterPageContext 15, // 12: abi.v1.RenderContext.author:type_name -> abi.v1.AuthorContext
21, // 13: abi.v1.RenderContext.injected_slots:type_name -> abi.v1.RenderContext.InjectedSlotsEntry 16, // 13: abi.v1.RenderContext.category:type_name -> abi.v1.CategoryContext
17, // 14: abi.v1.RenderContext.human_proof_banner:type_name -> abi.v1.HumanProofBanner 17, // 14: abi.v1.RenderContext.master_page:type_name -> abi.v1.MasterPageContext
18, // 15: abi.v1.RenderContext.detail_row:type_name -> abi.v1.DetailRow 22, // 15: abi.v1.RenderContext.injected_slots:type_name -> abi.v1.RenderContext.InjectedSlotsEntry
22, // 16: abi.v1.RequestInfo.headers:type_name -> abi.v1.RequestInfo.HeadersEntry 18, // 16: abi.v1.RenderContext.human_proof_banner:type_name -> abi.v1.HumanProofBanner
23, // 17: abi.v1.RequestInfo.cookies:type_name -> abi.v1.RequestInfo.CookiesEntry 19, // 17: abi.v1.RenderContext.detail_row:type_name -> abi.v1.DetailRow
27, // 18: abi.v1.PostContext.published_at:type_name -> google.protobuf.Timestamp 23, // 18: abi.v1.RequestInfo.headers:type_name -> abi.v1.RequestInfo.HeadersEntry
27, // 19: abi.v1.BlockContext.now:type_name -> google.protobuf.Timestamp 24, // 19: abi.v1.RequestInfo.cookies:type_name -> abi.v1.RequestInfo.CookiesEntry
24, // 20: abi.v1.BlockContext.query:type_name -> abi.v1.BlockContext.QueryEntry 28, // 20: abi.v1.PostContext.published_at:type_name -> google.protobuf.Timestamp
25, // 21: abi.v1.BlockContext.cookies:type_name -> abi.v1.BlockContext.CookiesEntry 28, // 21: abi.v1.BlockContext.now:type_name -> google.protobuf.Timestamp
26, // 22: abi.v1.BlockContext.headers:type_name -> abi.v1.BlockContext.HeadersEntry 25, // 22: abi.v1.BlockContext.query:type_name -> abi.v1.BlockContext.QueryEntry
23, // [23:23] is the sub-list for method output_type 26, // 23: abi.v1.BlockContext.cookies:type_name -> abi.v1.BlockContext.CookiesEntry
23, // [23:23] is the sub-list for method input_type 27, // 24: abi.v1.BlockContext.headers:type_name -> abi.v1.BlockContext.HeadersEntry
23, // [23:23] is the sub-list for extension type_name 25, // [25:25] is the sub-list for method output_type
23, // [23:23] is the sub-list for extension extendee 25, // [25:25] is the sub-list for method input_type
0, // [0:23] is the sub-list for field type_name 25, // [25:25] is the sub-list for extension type_name
25, // [25:25] is the sub-list for extension extendee
0, // [0:25] is the sub-list for field type_name
} }
func init() { file_v1_render_proto_init() } func init() { file_v1_render_proto_init() }
@ -2098,7 +2273,7 @@ func file_v1_render_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(), GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_v1_render_proto_rawDesc), len(file_v1_render_proto_rawDesc)), RawDescriptor: unsafe.Slice(unsafe.StringData(file_v1_render_proto_rawDesc), len(file_v1_render_proto_rawDesc)),
NumEnums: 0, NumEnums: 0,
NumMessages: 27, NumMessages: 28,
NumExtensions: 0, NumExtensions: 0,
NumServices: 0, NumServices: 0,
}, },

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"slices"
"testing" "testing"
"github.com/google/uuid" "github.com/google/uuid"
@ -97,10 +98,8 @@ func TestCaptchaVerifiedReadsTrustedHeader(t *testing.T) {
} }
func TestAllTrustedHeadersIncludesCaptcha(t *testing.T) { func TestAllTrustedHeadersIncludesCaptcha(t *testing.T) {
for _, h := range AllTrustedHeaders() { if slices.Contains(AllTrustedHeaders(), HeaderVerifiedCaptcha) {
if h == HeaderVerifiedCaptcha {
return return
} }
}
t.Fatalf("AllTrustedHeaders() missing %s — hosts strip-then-stamp from this list, so omitting it makes the header client-forgeable", HeaderVerifiedCaptcha) t.Fatalf("AllTrustedHeaders() missing %s — hosts strip-then-stamp from this list, so omitting it makes the header client-forgeable", HeaderVerifiedCaptcha)
} }

View File

@ -15,6 +15,8 @@ import (
type PageContext struct { type PageContext struct {
ID uuid.UUID ID uuid.UUID
Slug string Slug string
DerivedSlug string // resolved route from the page hierarchy
MountPath string // effective host-resolved mount prefix
Title string Title string
PostType string // "page", "post", "master", "system" PostType string // "page", "post", "master", "system"
Status string // "published", "draft", "scheduled" Status string // "published", "draft", "scheduled"
@ -371,6 +373,8 @@ type BlockContext struct {
URL string `pongo2:"url"` URL string `pongo2:"url"`
Path string `pongo2:"path"` Path string `pongo2:"path"`
Slug string `pongo2:"slug"` Slug string `pongo2:"slug"`
DerivedSlug string `pongo2:"derivedSlug"`
MountPath string `pongo2:"mountPath"`
PageID string `pongo2:"pageId"` PageID string `pongo2:"pageId"`
PageTitle string `pongo2:"pageTitle"` PageTitle string `pongo2:"pageTitle"`
TemplateKey string `pongo2:"templateKey"` TemplateKey string `pongo2:"templateKey"`
@ -425,6 +429,7 @@ func GetBlockContext(ctx context.Context) *BlockContext {
func (bc *BlockContext) ToMap() map[string]any { func (bc *BlockContext) ToMap() map[string]any {
return map[string]any{ return map[string]any{
"url": bc.URL, "path": bc.Path, "slug": bc.Slug, "url": bc.URL, "path": bc.Path, "slug": bc.Slug,
"derivedSlug": bc.DerivedSlug, "mountPath": bc.MountPath,
"pageId": bc.PageID, "pageTitle": bc.PageTitle, "pageId": bc.PageID, "pageTitle": bc.PageTitle,
"templateKey": bc.TemplateKey, "isEditor": bc.IsEditor, "templateKey": bc.TemplateKey, "isEditor": bc.IsEditor,
"timestamp": bc.Timestamp, "now": bc.Now, "timestamp": bc.Timestamp, "now": bc.Now,

View File

@ -3,6 +3,8 @@ package blocks
import ( import (
"encoding/json" "encoding/json"
"strings" "strings"
"git.dev.alexdunmow.com/block/pluginsdk/document"
) )
// poweredBlockSentinel prefixes a PoweredBlock marker string. It uses NUL // poweredBlockSentinel prefixes a PoweredBlock marker string. It uses NUL
@ -18,6 +20,15 @@ const poweredBlockSentinel = "\x00bn:powered\x00"
type PoweredResult struct { type PoweredResult struct {
Template string `json:"template"` Template string `json:"template"`
Data map[string]any `json:"data"` Data map[string]any `json:"data"`
StatusCode int32 `json:"statusCode,omitempty"`
Metadata *document.Metadata `json:"metadata,omitempty"`
}
// PoweredOptions controls host-owned response data for a powered block.
// StatusCode is transported as declared; the host accepts only 200599.
type PoweredOptions struct {
StatusCode int32
Metadata *document.Metadata
} }
// PoweredBlock marks a block's return value as "powered": instead of final // PoweredBlock marks a block's return value as "powered": instead of final
@ -37,11 +48,28 @@ type PoweredResult struct {
// hits ({% mytag %} / |myfilter) is a fresh RENDER_TAG / APPLY_FILTER invoke, // hits ({% mytag %} / |myfilter) is a fresh RENDER_TAG / APPLY_FILTER invoke,
// never a re-entrant one. // never a re-entrant one.
func PoweredBlock(template string, data map[string]any) string { func PoweredBlock(template string, data map[string]any) string {
payload, err := json.Marshal(PoweredResult{Template: template, Data: data}) return poweredBlockMarker(PoweredResult{Template: template, Data: data})
}
// PoweredBlockWithOptions is PoweredBlock with structured document metadata
// and an optional HTTP response status for the host to accumulate before it
// writes response headers.
func PoweredBlockWithOptions(template string, data map[string]any, options PoweredOptions) string {
return poweredBlockMarker(PoweredResult{
Template: template,
Data: data,
StatusCode: options.StatusCode,
Metadata: options.Metadata,
})
}
func poweredBlockMarker(result PoweredResult) string {
payload, err := json.Marshal(result)
if err != nil { if err != nil {
// A non-serializable data map is a programming error; fall back to an // A non-serializable data map is a programming error; fall back to an
// empty-data powered result so the template still renders. // empty-data result while preserving response options.
payload, _ = json.Marshal(PoweredResult{Template: template}) result.Data = nil
payload, _ = json.Marshal(result)
} }
return poweredBlockSentinel + string(payload) return poweredBlockSentinel + string(payload)
} }

View File

@ -1,6 +1,10 @@
package blocks package blocks
import "testing" import (
"testing"
"git.dev.alexdunmow.com/block/pluginsdk/document"
)
func TestPoweredBlockRoundTrip(t *testing.T) { func TestPoweredBlockRoundTrip(t *testing.T) {
marker := PoweredBlock("<p>{{ x }}</p>", map[string]any{"x": "y"}) marker := PoweredBlock("<p>{{ x }}</p>", map[string]any{"x": "y"})
@ -16,6 +20,47 @@ func TestPoweredBlockRoundTrip(t *testing.T) {
} }
} }
func TestPoweredBlockWithOptionsRoundTrip(t *testing.T) {
metadata := &document.Metadata{
Title: "Missing article",
Description: "The requested article was not found.",
CanonicalURL: "https://example.com/docs/missing",
ImageURL: "https://example.com/media/missing.png",
Robots: "noindex,nofollow",
}
marker := PoweredBlockWithOptions("<h1>{{ title }}</h1>", map[string]any{"title": "Missing"}, PoweredOptions{
StatusCode: 404,
Metadata: metadata,
})
got, ok := DecodePoweredBlock(marker)
if !ok {
t.Fatal("DecodePoweredBlock did not recognize its own marker")
}
if got.StatusCode != 404 {
t.Errorf("status code = %d, want 404", got.StatusCode)
}
if got.Metadata == nil || *got.Metadata != *metadata {
t.Errorf("metadata = %+v, want %+v", got.Metadata, metadata)
}
}
func TestPoweredBlockWithOptionsPreservesOptionsWhenDataCannotMarshal(t *testing.T) {
marker := PoweredBlockWithOptions("<h1>fallback</h1>", map[string]any{"bad": make(chan int)}, PoweredOptions{
StatusCode: 503,
Metadata: &document.Metadata{Title: "Unavailable"},
})
got, ok := DecodePoweredBlock(marker)
if !ok {
t.Fatal("DecodePoweredBlock did not recognize fallback marker")
}
if got.Data != nil {
t.Errorf("data = %#v, want nil fallback", got.Data)
}
if got.StatusCode != 503 || got.Metadata == nil || got.Metadata.Title != "Unavailable" {
t.Errorf("options were not preserved: %+v", got)
}
}
func TestDecodePoweredBlockRejectsPlainHTML(t *testing.T) { func TestDecodePoweredBlockRejectsPlainHTML(t *testing.T) {
if _, ok := DecodePoweredBlock("<p>hello</p>"); ok { if _, ok := DecodePoweredBlock("<p>hello</p>"); ok {
t.Error("plain HTML must not decode as a powered block") t.Error("plain HTML must not decode as a powered block")

View File

@ -0,0 +1,40 @@
# Keyed load-once hooks have durable host receipts
Plugin `Load` callbacks are deliberately repeatable: startup, enable, update,
and provisioning-consent replay can all invoke them. Some initialization is
different. A plugin may add a suggested menu link on its first load, but must
not restore that link after an administrator later removes or renames it.
Decision: `PluginRegistration.LoadOnce` is an ordered slice of callbacks with
stable keys. DESCRIBE publishes those keys in `PluginManifest.load_once_keys`.
The host checks a durable `(plugin, key)` receipt before dispatching
`HOOK_LOAD_ONCE`; it records the receipt only after success. A completed key is
not removed when the plugin is disabled, updated, or uninstalled. A later
release that intentionally needs another one-time action declares a new key.
Load-once runs after the ordinary `Load` hook so repeatable provisioning can
create resources that a one-time callback references. Keys are lowercase,
MCP-style stable identifiers of at most 128 bytes and must be unique within a
registration. Failed callbacks may be retried because no completion receipt
exists. As with database migrations, callbacks should still make their own
external mutation idempotent to cover a process failure between the mutation
and receipt commit.
Encoding first-install booleans in plugin settings was rejected because it
mixes host lifecycle state with customer-visible configuration. Making all
provisioner operations one-shot was rejected because pages, settings defaults,
and other declarations intentionally reconcile on every load. Version-based
execution was rejected because an unrelated release must not repeat the side
effect.
Consequences:
- Native and WASM plugins share one keyed lifecycle abstraction.
- The host, not guest memory, owns durable completion state.
- Updating or re-enabling a plugin cannot recreate an administrator-removed
resource under an already completed key.
- Changing a key is an explicit request to run a new callback once.
Keywords: PluginRegistration.LoadOnce, LoadOnceHook, load_once_keys,
HOOK_LOAD_ONCE, durable receipt, first load, plugin update, idempotency,
one-time provisioning, lifecycle

View File

@ -0,0 +1,58 @@
# Provisioned pages use stable keys and render results carry safe response data
Provisioned pages were identified only by slug. Slugs are customer-facing
content and can change when an administrator renames a page, changes its
hierarchy, or mounts a plugin below a customer-specific route. A later plugin
load could therefore fail to find its page, recreate it, or attach children
and menu items to a stale location.
Decision: a page declaration may carry a plugin-scoped `Key`. `ParentKey`
references another declaration from the same plugin, while menu items use the
pair `PagePlugin` and `PageKey` so they can target a provisioned page across
plugin boundaries. The existing `ParentSlug` field remains an explicit legacy
input, but keyed references are the stable identity for new declarations.
`MountPath` is create-only. The host applies it while creating the page but
does not relocate an existing customer page during later reconciliation.
`MergeMissingBlockContent` enables a shallow default merge for matching
plugin-owned block assignments: only absent top-level keys are added, and
present values such as an empty string, zero, false, or null are preserved.
The render ABI exposes both `DerivedSlug` and `MountPath` on page and block
contexts. `DerivedSlug` is computed from the resolved hierarchy. `MountPath`
is the effective, normalized value after host inheritance, rather than the raw
stored declaration. Guests consume these values and do not reimplement host
routing rules.
Powered blocks and template documents may also request an HTTP status and a
shared `DocumentMetadata` value. Metadata is deliberately limited to title,
description, canonical URL, image URL, and robots directives. The host
validates status codes and metadata, escapes values, and accumulates status
before writing headers. Only 200599 is accepted; the highest status class
wins, with the first result retained within a class. For metadata, the first
non-empty value for each field wins in deterministic render order, after which
page/document defaults fill gaps. Arbitrary meta/link maps were rejected
because they would create a second unvalidated head-injection path; advanced
trusted templates already have the explicit `HeadExtra` surface.
Appending fields to the v1 protobuf messages was chosen over renumbering or a
Go-only helper because `abi/proto/v1` is the language-neutral contract. Slug
fallbacks remain data in that contract; the SDK does not silently synthesize
keys from mutable slugs.
Consequences:
- Plugin updates can find the same provisioned page after customer route
changes without taking ownership of the customer's chosen slug.
- Parent and menu references survive page moves and renames.
- Changing a declaration's mount path does not unexpectedly move a live page.
- Plugins can add newly introduced block defaults without overwriting customer
configuration.
- Native and WASM render code receive the same host-resolved routing values.
- Wiki-like powered blocks can return correct not-found status and structured
metadata without rendering or injecting the document envelope themselves.
Keywords: PageConfig.Key, ParentKey, ParentSlug, MountPath,
MergeMissingBlockContent, MenuItemConfig.PagePlugin, PageKey, DerivedSlug,
PageSeed, RenderContext, PoweredBlockWithOptions, DocumentMetadata,
TemplateDocument, stable identity, page provisioning

View File

@ -0,0 +1,37 @@
# Plugin manifests declare admin API compatibility
Plugin admin bundles consume the versioned `@block-ninja/api` browser package.
The existing `block_core` compatibility constraint describes the host plugin
runtime, but it cannot tell a registry or CMS whether an admin bundle is safe
to load against the host's browser API. Treating those surfaces as one version
would couple independent release cycles and allow an otherwise compatible
plugin to fail only after its admin page loads.
Decision: `[compatibility]` gains an optional `admin_api` string. Its value is
a semantic-version constraint for the host-provided browser admin API, for
example `admin_api = ">=0.1.2"`. The manifest/parser layer preserves the value
verbatim; registry and host resolvers own constraint validation and matching.
An omitted or empty value means the plugin has not declared browser API
compatibility. It is not equivalent to an unconstrained wildcard. Automated
latest-compatible resolution that filters by a host admin API version must
therefore fail closed for an undeclared value. This does not change the
separate `block_core` constraint or imply that every plugin has an admin
bundle.
The field is part of the shared manifest model and the CLI's hand-written
`plugin.mod` serializer so version bumps, tag edits, and initialization cannot
silently discard it.
Consequences:
- Registries and hosts can select plugin releases compatible with both the
runtime ABI and browser admin API.
- Headless plugins can continue to omit `admin_api`.
- Plugin authors express a range rather than pinning a single browser package
version.
- Constraint syntax and matching remain the resolver's responsibility rather
than being duplicated in manifest parsers.
Keywords: plugin.mod, compatibility, admin_api, AdminAPI, @block-ninja/api,
semantic version, browser API, fail closed, plugin resolver

View File

@ -0,0 +1,27 @@
# Plugin manifests declare Plugin SDK compatibility
The historical `block_core` compatibility field predates the standalone
Plugin SDK and is ambiguous: CMS core and the plugin-facing SDK have separate
release cycles. Interpreting the same constraint against both versions can
select an artifact that the host cannot load or reject one that is compatible.
Decision: `[compatibility]` gains an optional `plugin_sdk` string containing a
semantic-version constraint for the plugin-facing SDK/ABI, for example
`plugin_sdk = ">=0.3.7"`. Manifest parsing preserves the value verbatim;
registry and host resolvers own constraint validation and matching.
An omitted or empty value is undeclared compatibility, not a wildcard.
Automated resolution against a host SDK version must fail closed, except for
an explicitly bounded migration policy for releases published before this
field existed. `block_core` remains a separate historical constraint and is
not reinterpreted as the CMS application's own version.
Consequences:
- Plugins can state the actual SDK/ABI range they require.
- CMS core can evolve independently of the public Plugin SDK.
- New releases must declare `plugin_sdk` to participate in automatic
SDK-compatible selection.
Keywords: plugin.mod, compatibility, plugin_sdk, PluginSDK, ABI, semantic
version, plugin resolver, fail closed

View File

@ -0,0 +1,27 @@
# Bridge providers receive authenticated caller identity
Bridge payloads are guest-controlled. A provider that uses a payload field as
an ownership namespace lets one plugin impersonate another plugin and mutate
or claim its managed records. The consumer-to-host `BridgeInvokeRequest`
cannot safely carry identity because the consumer constructs that message.
Decision: the host derives the caller plugin from the loaded module or native
plugin registration. It passes that identity to the provider in
`BridgeCallRequest.caller_plugin` and, for Go providers, through
`plugin.WithBridgeCallerPlugin`. Providers read it with
`plugin.BridgeCallerPlugin` and fail closed when an ownership-sensitive call
has no authenticated caller.
The new host-to-provider protobuf field is additive. Older providers ignore
it. New ownership-sensitive providers intentionally reject calls from older
hosts that cannot authenticate a caller. Hosts must never copy a caller name
from opaque bridge payloads or a consumer-authored capability field.
Consequences:
- Providers can derive durable ownership from an authenticated principal.
- Bridge payload schemas do not need security-sensitive source fields.
- Native and Wasm providers observe the same caller context contract.
Keywords: plugin bridge, caller identity, authentication, ownership,
BridgeCallRequest, WithBridgeCallerPlugin, Wasm, confused deputy

View File

@ -0,0 +1,36 @@
# Register sees the current guest host services
Go guest plugins can declare capability-backed runtime surfaces from their
`Register` callback. Bridge providers are the first such surface: the provider
stores its invokable value in the guest-local bridge stub while the stub tells
the CMS host that the named service exists.
`Serve` previously assigned the package-level guest runtime only after
`newGuest` returned. Because `newGuest` runs `Register` synchronously,
`HostServices()` returned an empty `CoreServices` value during registration.
Plugins that correctly nil-checked the bridge silently skipped registration.
The CMS could therefore mark a Wasm plugin loaded while consumers failed with
`bridge: no service registered` during a later load hook.
`newGuest` now makes the guest under construction current for the synchronous
registration pass and restores the previous runtime before returning. `Serve`
then installs the completed guest as before. This keeps construction isolated
for native tests while making the documented `HostServices()` escape hatch
truthful during `Register` on every pooled Wasm instance.
Alternatives rejected were moving bridge registration into `Load`, which runs
on only one pooled instance, and adding bridge names only to the manifest,
which would advertise host availability without installing the guest-local
`BridgeInvokable` value needed by `HOOK_BRIDGE_CALL`.
Consequences:
- Bridge services registered from `Register` exist on every pooled guest
instance and remain callable after CMS startup and hot swap.
- A regression test exercises the observable `Serve` plus
`HOOK_BRIDGE_CALL` contract instead of relying on initialization internals.
- No ABI or protobuf change is required; consumers need a plugin SDK release
containing the corrected Go guest runtime.
Keywords: wasmguest.Serve, newGuest, HostServices, PluginRegistration.Register,
plugin bridge, RegisterService, BridgeInvokable, HOOK_BRIDGE_CALL, wiki content

14
document/metadata.go Normal file
View File

@ -0,0 +1,14 @@
// Package document defines structured page-response metadata shared by block
// and template render results.
package document
// Metadata is the safe document-head subset a plugin may ask the host to
// render. The host remains responsible for validating URLs and robots values
// and for escaping every value before it is written into HTML.
type Metadata struct {
Title string `json:"title,omitempty"`
Description string `json:"description,omitempty"`
CanonicalURL string `json:"canonicalUrl,omitempty"`
ImageURL string `json:"imageUrl,omitempty"`
Robots string `json:"robots,omitempty"`
}

View File

@ -166,12 +166,12 @@ func validHostname(h string) bool {
if len(h) > 253 { if len(h) > 253 {
return false return false
} }
for _, label := range strings.Split(h, ".") { for label := range strings.SplitSeq(h, ".") {
if label == "" || len(label) > 63 { if label == "" || len(label) > 63 {
return false return false
} }
for _, r := range label { for _, r := range label {
if !(r >= 'a' && r <= 'z') && !(r >= '0' && r <= '9') && r != '-' { if (r < 'a' || r > 'z') && (r < '0' || r > '9') && r != '-' {
return false return false
} }
} }

View File

@ -2,6 +2,26 @@ package plugin
import "context" import "context"
type bridgeCallerPluginContextKey struct{}
// WithBridgeCallerPlugin records the host-authenticated plugin name of a
// bridge caller. Hosts and ABI adapters must derive pluginName from the loaded
// caller, never from guest-controlled request data.
func WithBridgeCallerPlugin(ctx context.Context, pluginName string) context.Context {
return context.WithValue(ctx, bridgeCallerPluginContextKey{}, pluginName)
}
// BridgeCallerPlugin returns the host-authenticated plugin name of the
// current bridge caller. Ownership-sensitive providers should fail closed
// when no caller identity is present.
func BridgeCallerPlugin(ctx context.Context) (string, bool) {
if ctx == nil {
return "", false
}
pluginName, ok := ctx.Value(bridgeCallerPluginContextKey{}).(string)
return pluginName, ok && pluginName != ""
}
// PluginBridge allows plugins to share services with each other. // PluginBridge allows plugins to share services with each other.
// Plugins register named services during startup; other plugins look them up at runtime. // Plugins register named services during startup; other plugins look them up at runtime.
// //

25
plugin/bridge_test.go Normal file
View File

@ -0,0 +1,25 @@
package plugin
import (
"context"
"testing"
)
func TestBridgeCallerPlugin(t *testing.T) {
if got, ok := BridgeCallerPlugin(context.Background()); ok || got != "" {
t.Fatalf("BridgeCallerPlugin(background) = %q, %t, want empty, false", got, ok)
}
ctx := WithBridgeCallerPlugin(context.Background(), "website")
if got, ok := BridgeCallerPlugin(ctx); !ok || got != "website" {
t.Fatalf("BridgeCallerPlugin(ctx) = %q, %t, want website, true", got, ok)
}
}
func TestWithBridgeCallerPluginEmptyDoesNotAuthenticateCaller(t *testing.T) {
ctx := WithBridgeCallerPlugin(context.Background(), "spoofed")
ctx = WithBridgeCallerPlugin(ctx, "")
if got, ok := BridgeCallerPlugin(ctx); ok || got != "" {
t.Fatalf("BridgeCallerPlugin(ctx) = %q, %t, want empty, false", got, ok)
}
}

26
plugin/load_once.go Normal file
View File

@ -0,0 +1,26 @@
package plugin
import (
"fmt"
"regexp"
)
const MaxLoadOnceKeyBytes = 128
var loadOnceKeyPattern = regexp.MustCompile(`^[a-z0-9][a-z0-9._-]*$`)
// ValidateLoadOnceKeys checks the stable identity contract shared by plugin
// authors, the packer, and the host loader.
func ValidateLoadOnceKeys(keys []string) error {
seen := make(map[string]bool, len(keys))
for _, key := range keys {
if len(key) == 0 || len(key) > MaxLoadOnceKeyBytes || !loadOnceKeyPattern.MatchString(key) {
return fmt.Errorf("invalid load-once key %q: must be 1-%d bytes of lowercase letters, digits, dot, underscore, or hyphen, starting alphanumeric", key, MaxLoadOnceKeyBytes)
}
if seen[key] {
return fmt.Errorf("duplicate load-once key %q", key)
}
seen[key] = true
}
return nil
}

27
plugin/load_once_test.go Normal file
View File

@ -0,0 +1,27 @@
package plugin
import "testing"
func TestValidateLoadOnceKeys(t *testing.T) {
tests := []struct {
name string
keys []string
ok bool
}{
{name: "valid stable keys", keys: []string{"documentation-menu.v1", "seed_defaults-2"}, ok: true},
{name: "empty", keys: []string{""}},
{name: "uppercase", keys: []string{"DocumentationMenu.v1"}},
{name: "duplicate", keys: []string{"menu.v1", "menu.v1"}},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
err := ValidateLoadOnceKeys(test.keys)
if test.ok && err != nil {
t.Fatalf("ValidateLoadOnceKeys() error = %v", err)
}
if !test.ok && err == nil {
t.Fatal("ValidateLoadOnceKeys() error = nil")
}
})
}
}

View File

@ -91,6 +91,8 @@ type ModPublicRoute struct {
type ModCompat struct { type ModCompat struct {
BlockCore string `toml:"block_core"` BlockCore string `toml:"block_core"`
AdminAPI string `toml:"admin_api"`
PluginSDK string `toml:"plugin_sdk"`
} }
type ModRequirement struct { type ModRequirement struct {

View File

@ -1,9 +1,12 @@
package plugin package plugin
import ( import (
"bytes"
"fmt" "fmt"
"strings" "strings"
"testing" "testing"
tomlpkg "github.com/BurntSushi/toml"
) )
func TestParseModFull_BasicFields(t *testing.T) { func TestParseModFull_BasicFields(t *testing.T) {
@ -278,6 +281,8 @@ version = "0.2.0"
[compatibility] [compatibility]
block_core = ">=1.5 <2.0" block_core = ">=1.5 <2.0"
admin_api = ">=0.1.2 <0.2.0"
plugin_sdk = ">=0.3.7 <0.4.0"
[[requires]] [[requires]]
name = "@blockninja/smartblock" name = "@blockninja/smartblock"
@ -291,8 +296,17 @@ version = ">=1.2"
if err != nil { if err != nil {
t.Fatalf("ParseModFull err: %v", err) t.Fatalf("ParseModFull err: %v", err)
} }
if m.Compatibility == nil || m.Compatibility.BlockCore != ">=1.5 <2.0" { if m.Compatibility == nil {
t.Errorf("Compat = %+v", m.Compatibility) t.Fatal("Compatibility is nil")
}
if m.Compatibility.BlockCore != ">=1.5 <2.0" {
t.Errorf("Compat.BlockCore = %q", m.Compatibility.BlockCore)
}
if m.Compatibility.AdminAPI != ">=0.1.2 <0.2.0" {
t.Errorf("Compat.AdminAPI = %q", m.Compatibility.AdminAPI)
}
if m.Compatibility.PluginSDK != ">=0.3.7 <0.4.0" {
t.Errorf("Compat.PluginSDK = %q", m.Compatibility.PluginSDK)
} }
if len(m.Requires) != 2 { if len(m.Requires) != 2 {
t.Fatalf("Requires len = %d, want 2", len(m.Requires)) t.Fatalf("Requires len = %d, want 2", len(m.Requires))
@ -305,6 +319,33 @@ version = ">=1.2"
} }
} }
func TestModCompatPluginSDKRoundTrip(t *testing.T) {
t.Parallel()
want := &ModFile{
Plugin: ModPlugin{Name: "wiki", Version: "0.2.0"},
Compatibility: &ModCompat{
BlockCore: ">=0.3.4",
AdminAPI: ">=0.1.2",
PluginSDK: ">=0.3.7 <0.4.0",
},
}
var encoded bytes.Buffer
if err := tomlpkg.NewEncoder(&encoded).Encode(want); err != nil {
t.Fatalf("encode plugin.mod: %v", err)
}
if !strings.Contains(encoded.String(), `plugin_sdk = ">=0.3.7 <0.4.0"`) {
t.Fatalf("encoded plugin.mod omitted plugin_sdk:\n%s", encoded.String())
}
got, err := ParseModFull(encoded.Bytes())
if err != nil {
t.Fatalf("ParseModFull(round trip): %v", err)
}
if got.Compatibility == nil || got.Compatibility.PluginSDK != want.Compatibility.PluginSDK {
t.Fatalf("round-trip PluginSDK = %#v, want %q", got.Compatibility, want.Compatibility.PluginSDK)
}
}
func TestNormalizeTags_HappyPath(t *testing.T) { func TestNormalizeTags_HappyPath(t *testing.T) {
got, err := NormalizeTags([]string{"dark", "agency", "serif"}) got, err := NormalizeTags([]string{"dark", "agency", "serif"})
if err != nil { if err != nil {

View File

@ -41,8 +41,24 @@ type DataTableConfig struct {
// PageConfig defines a page to provision. // PageConfig defines a page to provision.
type PageConfig struct { type PageConfig struct {
// Key is the stable, plugin-scoped identity used to find the page across
// slug and hierarchy changes.
Key string
Slug string Slug string
// ParentKey identifies a parent provisioned by the same plugin.
ParentKey string
// ParentSlug is the legacy slug-based parent reference. New declarations
// should use ParentKey so moving or renaming the parent does not break the
// relationship.
ParentSlug string ParentSlug string
// MountPath is applied when the page is first created. Reconciliation does
// not move an existing page when this value later changes.
MountPath string
Title string Title string
TemplateKey string TemplateKey string
Blocks []PageBlockConfig Blocks []PageBlockConfig
@ -51,6 +67,10 @@ type PageConfig struct {
DetailSlugField string DetailSlugField string
ReconcileBlocks bool ReconcileBlocks bool
ReconcileTemplate bool ReconcileTemplate bool
// MergeMissingBlockContent shallowly adds absent top-level default keys to
// matching plugin-owned block assignments. Present values, including empty
// strings, zero, false, and null, are preserved.
MergeMissingBlockContent bool
} }
// PageBlockConfig defines a block within a provisioned page. // PageBlockConfig defines a block within a provisioned page.
@ -75,6 +95,11 @@ type MenuItemConfig struct {
Label string Label string
URL string URL string
PageSlug string PageSlug string
// PagePlugin and PageKey identify a provisioned page independently of its
// current slug. PagePlugin permits a menu declared by one plugin to target
// a page owned by another.
PagePlugin string
PageKey string
SortOrder int32 SortOrder int32
} }

View File

@ -68,7 +68,7 @@ func checkRoutePath(r ModPublicRoute) string {
if strings.HasSuffix(p, "/") { if strings.HasSuffix(p, "/") {
return "no trailing slash; set prefix = true to claim the subtree" return "no trailing slash; set prefix = true to claim the subtree"
} }
for _, seg := range strings.Split(p[1:], "/") { for seg := range strings.SplitSeq(p[1:], "/") {
switch { switch {
case seg == "": case seg == "":
return "empty path segment" return "empty path segment"

View File

@ -13,6 +13,15 @@ import (
// Plugins register their templates and blocks through the provided registries. // Plugins register their templates and blocks through the provided registries.
type RegisterFunc func(tr templates.TemplateRegistry, br blocks.BlockRegistry) error type RegisterFunc func(tr templates.TemplateRegistry, br blocks.BlockRegistry) error
// LoadOnceHook is a persistent, keyed load callback. The host runs Run after
// the ordinary Load hook and records a successful execution by plugin name and
// Key. The same key is never run successfully again on that site; changing the
// key explicitly schedules a replacement callback once.
type LoadOnceHook struct {
Key string
Run func(deps CoreServices) error
}
// PluginRegistration defines a compiled-in plugin's entry points. // PluginRegistration defines a compiled-in plugin's entry points.
type PluginRegistration struct { type PluginRegistration struct {
Name string Name string
@ -40,6 +49,7 @@ type PluginRegistration struct {
MediaHooks MediaHooksProvider MediaHooks MediaHooksProvider
Load func(deps CoreServices) error Load func(deps CoreServices) error
LoadOnce []LoadOnceHook
Unload func(ctx context.Context) error Unload func(ctx context.Context) error
Dependencies []Dependency Dependencies []Dependency

View File

@ -790,7 +790,9 @@ func TestCapabilityRoundTrip(t *testing.T) {
resp: &abiv1.ProvisionerEnsurePageResponse{}, resp: &abiv1.ProvisionerEnsurePageResponse{},
run: func(t *testing.T, cs plugin.CoreServices) { run: func(t *testing.T, cs plugin.CoreServices) {
err := cs.Provisioner.EnsurePage(plugin.PageConfig{ err := cs.Provisioner.EnsurePage(plugin.PageConfig{
Slug: "/", Title: "Home", TemplateKey: "homepage", ReconcileBlocks: true, Key: "home", Slug: "/", ParentKey: "site", ParentSlug: "/legacy-site",
MountPath: "/knowledge", Title: "Home", TemplateKey: "homepage",
ReconcileBlocks: true, MergeMissingBlockContent: true,
Blocks: []plugin.PageBlockConfig{ Blocks: []plugin.PageBlockConfig{
{BlockKey: "html", Title: "Hero", Content: map[string]any{"x": float64(1)}, Slot: "main", SortOrder: 1}, {BlockKey: "html", Title: "Hero", Content: map[string]any{"x": float64(1)}, Slot: "main", SortOrder: 1},
}, },
@ -813,7 +815,9 @@ func TestCapabilityRoundTrip(t *testing.T) {
name: "provisioner_ensure_menu_item", wantMethod: "provisioner.ensure_menu_item", name: "provisioner_ensure_menu_item", wantMethod: "provisioner.ensure_menu_item",
resp: &abiv1.ProvisionerEnsureMenuItemResponse{}, resp: &abiv1.ProvisionerEnsureMenuItemResponse{},
run: func(t *testing.T, cs plugin.CoreServices) { run: func(t *testing.T, cs plugin.CoreServices) {
err := cs.Provisioner.EnsureMenuItem("main", plugin.MenuItemConfig{Label: "Docs", PageSlug: "/docs", SortOrder: 3}) err := cs.Provisioner.EnsureMenuItem("main", plugin.MenuItemConfig{
Label: "Docs", PageSlug: "/docs", PagePlugin: "wiki", PageKey: "index", SortOrder: 3,
})
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -1008,6 +1012,41 @@ func TestCapabilityRoundTrip(t *testing.T) {
} }
} }
func TestProvisionerPageContractGolden(t *testing.T) {
raw, err := os.ReadFile(goldenPath("provisioner_ensure_page_req"))
if err != nil {
t.Fatal(err)
}
req := &abiv1.ProvisionerEnsurePageRequest{}
if err := proto.Unmarshal(raw, req); err != nil {
t.Fatal(err)
}
page := req.GetPage()
if page == nil {
t.Fatal("page is nil")
}
if page.GetKey() != "home" || page.GetSlug() != "/" ||
page.GetParentKey() != "site" || page.GetParentSlug() != "/legacy-site" ||
page.GetMountPath() != "/knowledge" || !page.GetMergeMissingBlockContent() {
t.Errorf("page seed = %+v", page)
}
}
func TestProvisionerMenuPageReferenceContractGolden(t *testing.T) {
raw, err := os.ReadFile(goldenPath("provisioner_ensure_menu_item_req"))
if err != nil {
t.Fatal(err)
}
req := &abiv1.ProvisionerEnsureMenuItemRequest{}
if err := proto.Unmarshal(raw, req); err != nil {
t.Fatal(err)
}
if req.GetMenuName() != "main" || req.GetPageSlug() != "/docs" ||
req.GetPagePlugin() != "wiki" || req.GetPageKey() != "index" {
t.Errorf("menu request = %+v", req)
}
}
// fakeAbiErr is a transport error carrying an ABI code, like wasmguest.HostError. // fakeAbiErr is a transport error carrying an ABI code, like wasmguest.HostError.
type fakeAbiErr struct { type fakeAbiErr struct {
code abiv1.AbiErrorCode code abiv1.AbiErrorCode

View File

@ -53,8 +53,11 @@ func (s *provisionerStub) EnsureSetting(key string, defaultValue any) error {
func (s *provisionerStub) EnsurePage(config plugin.PageConfig) error { func (s *provisionerStub) EnsurePage(config plugin.PageConfig) error {
seed := &abiv1.PageSeed{ seed := &abiv1.PageSeed{
Key: config.Key,
Slug: config.Slug, Slug: config.Slug,
ParentKey: config.ParentKey,
ParentSlug: config.ParentSlug, ParentSlug: config.ParentSlug,
MountPath: config.MountPath,
Title: config.Title, Title: config.Title,
TemplateKey: config.TemplateKey, TemplateKey: config.TemplateKey,
DetailSourceType: config.DetailSourceType, DetailSourceType: config.DetailSourceType,
@ -62,6 +65,7 @@ func (s *provisionerStub) EnsurePage(config plugin.PageConfig) error {
DetailSlugField: config.DetailSlugField, DetailSlugField: config.DetailSlugField,
ReconcileBlocks: config.ReconcileBlocks, ReconcileBlocks: config.ReconcileBlocks,
ReconcileTemplate: config.ReconcileTemplate, ReconcileTemplate: config.ReconcileTemplate,
MergeMissingBlockContent: config.MergeMissingBlockContent,
} }
for _, b := range config.Blocks { for _, b := range config.Blocks {
contentJSON, err := json.Marshal(b.Content) contentJSON, err := json.Marshal(b.Content)
@ -96,6 +100,8 @@ func (s *provisionerStub) EnsureMenuItem(menuName string, config plugin.MenuItem
Label: config.Label, Label: config.Label,
Url: config.URL, Url: config.URL,
PageSlug: config.PageSlug, PageSlug: config.PageSlug,
PagePlugin: config.PagePlugin,
PageKey: config.PageKey,
SortOrder: config.SortOrder, SortOrder: config.SortOrder,
} }
return s.invoke(context.Background(), "ensure_menu_item", req, &abiv1.ProvisionerEnsureMenuItemResponse{}) return s.invoke(context.Background(), "ensure_menu_item", req, &abiv1.ProvisionerEnsureMenuItemResponse{})

View File

@ -1,2 +1,2 @@
mainDocs"/docs( mainDocs"/docs(2wiki:index

View File

@ -1,4 +1,5 @@
4 \
/Home"homepage* / /legacy-siteHome"homepage*
htmlHero{"x":1}*main0H htmlHero{"x":1}*main0HZhomebsitej
/knowledgep

View File

@ -39,6 +39,8 @@ func contextFromRenderContext(ctx context.Context, rc *abiv1.RenderContext) cont
ctx = blocks.WithCurrentPage(ctx, &blocks.PageContext{ ctx = blocks.WithCurrentPage(ctx, &blocks.PageContext{
ID: parseUUID(p.GetId()), ID: parseUUID(p.GetId()),
Slug: p.GetSlug(), Slug: p.GetSlug(),
DerivedSlug: p.GetDerivedSlug(),
MountPath: p.GetMountPath(),
Title: p.GetTitle(), Title: p.GetTitle(),
PostType: p.GetPostType(), PostType: p.GetPostType(),
Status: p.GetStatus(), Status: p.GetStatus(),
@ -159,6 +161,8 @@ func blockContextFromProto(pb *abiv1.BlockContext) *blocks.BlockContext {
URL: pb.GetUrl(), URL: pb.GetUrl(),
Path: pb.GetPath(), Path: pb.GetPath(),
Slug: pb.GetSlug(), Slug: pb.GetSlug(),
DerivedSlug: pb.GetDerivedSlug(),
MountPath: pb.GetMountPath(),
PageID: pb.GetPageId(), PageID: pb.GetPageId(),
PageTitle: pb.GetPageTitle(), PageTitle: pb.GetPageTitle(),
TemplateKey: pb.GetTemplateKey(), TemplateKey: pb.GetTemplateKey(),

View File

@ -0,0 +1,51 @@
package wasmguest
import (
"context"
"testing"
abiv1 "git.dev.alexdunmow.com/block/pluginsdk/abi/v1"
"git.dev.alexdunmow.com/block/pluginsdk/blocks"
"github.com/google/uuid"
)
func TestContextFromRenderContextCarriesResolvedPagePaths(t *testing.T) {
pageID := uuid.MustParse("5f0c5f3f-3f1e-4a3a-9b6e-2f4f6a6d7e8f")
ctx := contextFromRenderContext(context.Background(), &abiv1.RenderContext{
Page: &abiv1.PageContext{
Id: pageID.String(),
Slug: "/article",
DerivedSlug: "/knowledge/article",
MountPath: "/knowledge",
Title: "Article",
PostType: "page",
Status: "published",
},
BlockContext: &abiv1.BlockContext{
Slug: "/article",
DerivedSlug: "/knowledge/article",
MountPath: "/knowledge",
},
})
page := blocks.GetCurrentPage(ctx)
if page == nil {
t.Fatal("current page is nil")
}
if page.ID != pageID || page.Slug != "/article" ||
page.DerivedSlug != "/knowledge/article" || page.MountPath != "/knowledge" {
t.Errorf("page context = %+v", page)
}
block := blocks.GetBlockContext(ctx)
if block == nil {
t.Fatal("block context is nil")
}
if block.Slug != "/article" || block.DerivedSlug != "/knowledge/article" || block.MountPath != "/knowledge" {
t.Errorf("block context = %+v", block)
}
data := block.ToMap()
if data["derivedSlug"] != "/knowledge/article" || data["mountPath"] != "/knowledge" {
t.Errorf("block template map = %#v", data)
}
}

View File

@ -147,6 +147,9 @@ func (g *guest) buildManifest() *abiv1.PluginManifest {
} }
} }
m.RequiredIconPacks = append(m.RequiredIconPacks, reg.RequiredIconPacks...) m.RequiredIconPacks = append(m.RequiredIconPacks, reg.RequiredIconPacks...)
for _, hook := range reg.LoadOnce {
m.LoadOnceKeys = append(m.LoadOnceKeys, hook.Key)
}
// Custom template tags/filters the plugin registered via blocks.RegisterTag // Custom template tags/filters the plugin registered via blocks.RegisterTag
// / blocks.RegisterFilter during the Register pass (runRegister reset the // / blocks.RegisterFilter during the Register pass (runRegister reset the

View File

@ -14,6 +14,7 @@ import (
abiv1 "git.dev.alexdunmow.com/block/pluginsdk/abi/v1" abiv1 "git.dev.alexdunmow.com/block/pluginsdk/abi/v1"
"git.dev.alexdunmow.com/block/pluginsdk/ai" "git.dev.alexdunmow.com/block/pluginsdk/ai"
"git.dev.alexdunmow.com/block/pluginsdk/blocks" "git.dev.alexdunmow.com/block/pluginsdk/blocks"
"git.dev.alexdunmow.com/block/pluginsdk/document"
"git.dev.alexdunmow.com/block/pluginsdk/plugin" "git.dev.alexdunmow.com/block/pluginsdk/plugin"
"git.dev.alexdunmow.com/block/pluginsdk/plugin/wasmguest/bnwasm" "git.dev.alexdunmow.com/block/pluginsdk/plugin/wasmguest/bnwasm"
"git.dev.alexdunmow.com/block/pluginsdk/plugin/wasmguest/caps" "git.dev.alexdunmow.com/block/pluginsdk/plugin/wasmguest/caps"
@ -99,7 +100,15 @@ func newGuest(reg plugin.PluginRegistration) *guest {
// Pool whose calls fail cleanly, matching the capability stubs. // Pool whose calls fail cleanly, matching the capability stubs.
g.services.Pool = bnwasm.NewPool(bnwasm.Transport(capTransport)) g.services.Pool = bnwasm.NewPool(bnwasm.Transport(capTransport))
g.rag, _ = g.services.RAGService.(*caps.RAGStub) g.rag, _ = g.services.RAGService.(*caps.RAGStub)
// Register callbacks may use HostServices for capability-backed surfaces
// that are not passed as Register parameters, such as bridge services.
// Publish this partially initialized guest only for the synchronous
// registration pass, then restore the previous runtime. Serve installs g
// permanently after newGuest returns.
previous := current
current = g
g.registerErr = runRegister(reg, g.templates, g.blocks) g.registerErr = runRegister(reg, g.templates, g.blocks)
current = previous
return g return g
} }
@ -185,6 +194,8 @@ func (g *guest) invoke(hookID uint32, req []byte) (out []byte) {
payload, abiErr = g.runJob(ctx, env.GetPayload()) payload, abiErr = g.runJob(ctx, env.GetPayload())
case abiv1.Hook_HOOK_LOAD: case abiv1.Hook_HOOK_LOAD:
payload, abiErr = g.load(env.GetPayload()) payload, abiErr = g.load(env.GetPayload())
case abiv1.Hook_HOOK_LOAD_ONCE:
payload, abiErr = g.loadOnce(env.GetPayload())
case abiv1.Hook_HOOK_UNLOAD: case abiv1.Hook_HOOK_UNLOAD:
payload, abiErr = g.unload(ctx) payload, abiErr = g.unload(ctx)
case abiv1.Hook_HOOK_RAG_FETCH: case abiv1.Hook_HOOK_RAG_FETCH:
@ -251,7 +262,12 @@ func (g *guest) renderBlock(ctx context.Context, payload []byte) (proto.Message,
return nil, internalError("marshal powered block data: " + err.Error()) return nil, internalError("marshal powered block data: " + err.Error())
} }
return &abiv1.RenderBlockResponse{ return &abiv1.RenderBlockResponse{
Powered: &abiv1.PoweredBlock{Template: pr.Template, DataJson: dataJSON}, Powered: &abiv1.PoweredBlock{
Template: pr.Template,
DataJson: dataJSON,
StatusCode: pr.StatusCode,
Metadata: documentMetadataToProto(pr.Metadata),
},
}, nil }, nil
} }
return &abiv1.RenderBlockResponse{Html: out}, nil return &abiv1.RenderBlockResponse{Html: out}, nil
@ -362,6 +378,8 @@ func (g *guest) renderTemplate(ctx context.Context, payload []byte) (proto.Messa
tdoc.HtmlAttrs = pd.HTMLAttrs tdoc.HtmlAttrs = pd.HTMLAttrs
tdoc.HeadExtraHtml = head tdoc.HeadExtraHtml = head
tdoc.BodyEndExtraHtml = end tdoc.BodyEndExtraHtml = end
tdoc.StatusCode = pd.StatusCode
tdoc.Metadata = documentMetadataToProto(pd.Metadata)
} else { } else {
body, err := renderPart(component) body, err := renderPart(component)
if err != nil { if err != nil {
@ -372,6 +390,19 @@ func (g *guest) renderTemplate(ctx context.Context, payload []byte) (proto.Messa
return &abiv1.RenderTemplateResponse{Document: tdoc}, nil return &abiv1.RenderTemplateResponse{Document: tdoc}, nil
} }
func documentMetadataToProto(metadata *document.Metadata) *abiv1.DocumentMetadata {
if metadata == nil {
return nil
}
return &abiv1.DocumentMetadata{
Title: metadata.Title,
Description: metadata.Description,
CanonicalUrl: metadata.CanonicalURL,
ImageUrl: metadata.ImageURL,
Robots: metadata.Robots,
}
}
func (g *guest) handleHTTP(ctx context.Context, payload []byte) (proto.Message, *abiv1.AbiError) { func (g *guest) handleHTTP(ctx context.Context, payload []byte) (proto.Message, *abiv1.AbiError) {
if g.reg.HTTPHandler == nil { if g.reg.HTTPHandler == nil {
return nil, &abiv1.AbiError{ return nil, &abiv1.AbiError{
@ -470,6 +501,29 @@ func (g *guest) load(payload []byte) (proto.Message, *abiv1.AbiError) {
return &abiv1.LoadResponse{}, nil return &abiv1.LoadResponse{}, nil
} }
func (g *guest) loadOnce(payload []byte) (proto.Message, *abiv1.AbiError) {
req := &abiv1.LoadOnceRequest{}
if err := proto.Unmarshal(payload, req); err != nil {
return nil, decodeError("LoadOnceRequest", err)
}
for _, hook := range g.reg.LoadOnce {
if hook.Key != req.GetKey() {
continue
}
if hook.Run == nil {
return nil, internalError("load-once hook " + hook.Key + " has no callback")
}
if err := hook.Run(g.services); err != nil {
return nil, internalError(err.Error())
}
return &abiv1.LoadOnceResponse{}, nil
}
return nil, &abiv1.AbiError{
Code: abiv1.AbiErrorCode_ABI_ERROR_CODE_UNIMPLEMENTED,
Message: "unknown load-once key " + req.GetKey(),
}
}
func (g *guest) unload(ctx context.Context) (proto.Message, *abiv1.AbiError) { func (g *guest) unload(ctx context.Context) (proto.Message, *abiv1.AbiError) {
if g.reg.Unload != nil { if g.reg.Unload != nil {
if err := g.reg.Unload(ctx); err != nil { if err := g.reg.Unload(ctx); err != nil {
@ -667,6 +721,7 @@ func (g *guest) bridgeCall(ctx context.Context, payload []byte) (proto.Message,
Message: "bridge service " + req.GetServiceName() + " does not implement plugin.BridgeInvokable", Message: "bridge service " + req.GetServiceName() + " does not implement plugin.BridgeInvokable",
} }
} }
ctx = plugin.WithBridgeCallerPlugin(ctx, req.GetCallerPlugin())
out, err := invokable.InvokeBridge(ctx, req.GetMethod(), req.GetPayload()) out, err := invokable.InvokeBridge(ctx, req.GetMethod(), req.GetPayload())
if err != nil { if err != nil {
return nil, internalError(err.Error()) return nil, internalError(err.Error())

View File

@ -10,6 +10,7 @@ import (
abiv1 "git.dev.alexdunmow.com/block/pluginsdk/abi/v1" abiv1 "git.dev.alexdunmow.com/block/pluginsdk/abi/v1"
"git.dev.alexdunmow.com/block/pluginsdk/blocks" "git.dev.alexdunmow.com/block/pluginsdk/blocks"
"git.dev.alexdunmow.com/block/pluginsdk/document"
"git.dev.alexdunmow.com/block/pluginsdk/plugin" "git.dev.alexdunmow.com/block/pluginsdk/plugin"
"git.dev.alexdunmow.com/block/pluginsdk/templates" "git.dev.alexdunmow.com/block/pluginsdk/templates"
"google.golang.org/protobuf/proto" "google.golang.org/protobuf/proto"
@ -24,6 +25,12 @@ func (c textComponent) Render(_ context.Context, w io.Writer) error {
return err return err
} }
type bridgeInvokableFunc func(context.Context, string, []byte) ([]byte, error)
func (f bridgeInvokableFunc) InvokeBridge(ctx context.Context, method string, payload []byte) ([]byte, error) {
return f(ctx, method, payload)
}
func fixtureRegistration() plugin.PluginRegistration { func fixtureRegistration() plugin.PluginRegistration {
return plugin.PluginRegistration{ return plugin.PluginRegistration{
Name: "fixture", Name: "fixture",
@ -123,6 +130,76 @@ func TestServeInstallsRuntime(t *testing.T) {
} }
} }
func TestServeExposesHostServicesDuringRegister(t *testing.T) {
previous := current
t.Cleanup(func() { current = previous })
Serve(plugin.PluginRegistration{
Name: "bridge-provider",
Version: "0.1.0",
Register: func(_ templates.TemplateRegistry, _ blocks.BlockRegistry) error {
bridge := HostServices().Bridge
if bridge == nil {
return fmt.Errorf("HostServices bridge is unavailable during Register")
}
bridge.RegisterService("bridge-provider", "content", bridgeInvokableFunc(
func(_ context.Context, _ string, _ []byte) ([]byte, error) {
return []byte("registered"), nil
},
))
return nil
},
})
resp := invokeHook(t, current, abiv1.Hook_HOOK_BRIDGE_CALL, &abiv1.BridgeCallRequest{
ServiceName: "content",
Method: "apply",
})
if resp.GetError() != nil {
t.Fatalf("bridge call returned error: %v", resp.GetError())
}
bridgeResp := &abiv1.BridgeCallResponse{}
if err := proto.Unmarshal(resp.GetPayload(), bridgeResp); err != nil {
t.Fatalf("unmarshal BridgeCallResponse: %v", err)
}
if got := string(bridgeResp.GetPayload()); got != "registered" {
t.Fatalf("bridge payload = %q, want registered", got)
}
}
func TestBridgeCallProvidesAuthenticatedCallerContext(t *testing.T) {
g := newGuest(fixtureRegistration())
g.services.Bridge.RegisterService("fixture", "content", bridgeInvokableFunc(
func(ctx context.Context, method string, payload []byte) ([]byte, error) {
caller, ok := plugin.BridgeCallerPlugin(ctx)
if !ok || caller != "website" {
t.Fatalf("BridgeCallerPlugin(ctx) = %q, %t, want website, true", caller, ok)
}
if method != "ApplyBundle" || string(payload) != `{"revision":"1"}` {
t.Fatalf("bridge call = %q %q", method, payload)
}
return []byte(`{"created":1}`), nil
},
))
resp := invokeHook(t, g, abiv1.Hook_HOOK_BRIDGE_CALL, &abiv1.BridgeCallRequest{
ServiceName: "content",
Method: "ApplyBundle",
Payload: []byte(`{"revision":"1"}`),
CallerPlugin: "website",
})
if resp.GetError() != nil {
t.Fatalf("bridge call returned error: %v", resp.GetError())
}
bridgeResp := &abiv1.BridgeCallResponse{}
if err := proto.Unmarshal(resp.GetPayload(), bridgeResp); err != nil {
t.Fatalf("unmarshal BridgeCallResponse: %v", err)
}
if got := string(bridgeResp.GetPayload()); got != `{"created":1}` {
t.Fatalf("bridge payload = %q, want report", got)
}
}
func TestDescribeManifest(t *testing.T) { func TestDescribeManifest(t *testing.T) {
g := newGuest(fixtureRegistration()) g := newGuest(fixtureRegistration())
resp := invokeHook(t, g, abiv1.Hook_HOOK_DESCRIBE, &abiv1.DescribeRequest{HostAbiVersion: 1}) resp := invokeHook(t, g, abiv1.Hook_HOOK_DESCRIBE, &abiv1.DescribeRequest{HostAbiVersion: 1})
@ -383,6 +460,14 @@ func TestRenderTemplatePageDocumentFillsEnvelope(t *testing.T) {
HTMLAttrs: map[string]string{"data-theme": "x"}, HTMLAttrs: map[string]string{"data-theme": "x"},
HeadExtra: textComponent(`<meta name="e">`), HeadExtra: textComponent(`<meta name="e">`),
BodyEndExtra: textComponent("<script>1</script>"), BodyEndExtra: textComponent("<script>1</script>"),
StatusCode: 201,
Metadata: &document.Metadata{
Title: "Page title",
Description: "Page description",
CanonicalURL: "https://example.com/page",
ImageURL: "https://example.com/page.png",
Robots: "index,follow",
},
} }
}) })
}, },
@ -419,6 +504,17 @@ func TestRenderTemplatePageDocumentFillsEnvelope(t *testing.T) {
if string(doc.GetBodyEndExtraHtml()) != "<script>1</script>" { if string(doc.GetBodyEndExtraHtml()) != "<script>1</script>" {
t.Errorf("body_end_extra_html = %q", doc.GetBodyEndExtraHtml()) t.Errorf("body_end_extra_html = %q", doc.GetBodyEndExtraHtml())
} }
if doc.GetStatusCode() != 201 {
t.Errorf("status_code = %d, want 201", doc.GetStatusCode())
}
metadata := doc.GetMetadata()
if metadata == nil || metadata.GetTitle() != "Page title" ||
metadata.GetDescription() != "Page description" ||
metadata.GetCanonicalUrl() != "https://example.com/page" ||
metadata.GetImageUrl() != "https://example.com/page.png" ||
metadata.GetRobots() != "index,follow" {
t.Errorf("metadata = %+v", metadata)
}
} }
func TestJobHook(t *testing.T) { func TestJobHook(t *testing.T) {
@ -451,3 +547,41 @@ func TestLoadUnloadHooks(t *testing.T) {
t.Fatalf("unload error: %v", resp.GetError()) t.Fatalf("unload error: %v", resp.GetError())
} }
} }
func TestDescribeAndDispatchLoadOnceHook(t *testing.T) {
calls := 0
reg := fixtureRegistration()
reg.LoadOnce = []plugin.LoadOnceHook{{
Key: "documentation-menu.v1",
Run: func(plugin.CoreServices) error {
calls++
return nil
},
}}
g := newGuest(reg)
describe := invokeHook(t, g, abiv1.Hook_HOOK_DESCRIBE, &abiv1.DescribeRequest{HostAbiVersion: 1})
if describe.GetError() != nil {
t.Fatalf("describe error: %v", describe.GetError())
}
dr := &abiv1.DescribeResponse{}
if err := proto.Unmarshal(describe.GetPayload(), dr); err != nil {
t.Fatalf("unmarshal DescribeResponse: %v", err)
}
if got := dr.GetManifest().GetLoadOnceKeys(); len(got) != 1 || got[0] != "documentation-menu.v1" {
t.Fatalf("load_once_keys = %v", got)
}
resp := invokeHook(t, g, abiv1.Hook_HOOK_LOAD_ONCE, &abiv1.LoadOnceRequest{Key: "documentation-menu.v1"})
if resp.GetError() != nil {
t.Fatalf("load-once error: %v", resp.GetError())
}
if calls != 1 {
t.Fatalf("calls = %d, want 1", calls)
}
resp = invokeHook(t, g, abiv1.Hook_HOOK_LOAD_ONCE, &abiv1.LoadOnceRequest{Key: "unknown.v1"})
if resp.GetError().GetCode() != abiv1.AbiErrorCode_ABI_ERROR_CODE_UNIMPLEMENTED {
t.Fatalf("unknown key error = %v, want UNIMPLEMENTED", resp.GetError())
}
}

View File

@ -74,8 +74,8 @@ func methodDocumentation(method protoreflect.MethodDescriptor) (description, doc
} }
paragraph = strings.TrimSpace(strings.ReplaceAll(paragraph, "\n", " ")) paragraph = strings.TrimSpace(strings.ReplaceAll(paragraph, "\n", " "))
methodName := string(method.Name()) methodName := string(method.Name())
if strings.HasPrefix(paragraph, methodName+" ") { if after, ok := strings.CutPrefix(paragraph, methodName+" "); ok {
paragraph = strings.TrimSpace(strings.TrimPrefix(paragraph, methodName+" ")) paragraph = strings.TrimSpace(after)
if paragraph != "" { if paragraph != "" {
runes := []rune(paragraph) runes := []rune(paragraph)
runes[0] = unicode.ToUpper(runes[0]) runes[0] = unicode.ToUpper(runes[0])

View File

@ -65,30 +65,30 @@ func registerMCPFixture(t *testing.T) {
return return
} }
file, err := protodesc.NewFile(&descriptorpb.FileDescriptorProto{ file, err := protodesc.NewFile(&descriptorpb.FileDescriptorProto{
Name: proto.String("test/mcpfixture.proto"), Name: new("test/mcpfixture.proto"),
Package: proto.String("mcpfixture.v1"), Package: new("mcpfixture.v1"),
Syntax: proto.String("proto3"), Syntax: new("proto3"),
Dependency: []string{"google/protobuf/timestamp.proto"}, Dependency: []string{"google/protobuf/timestamp.proto"},
MessageType: []*descriptorpb.DescriptorProto{{ MessageType: []*descriptorpb.DescriptorProto{{
Name: proto.String("ListArticlesRequest"), Name: new("ListArticlesRequest"),
Field: []*descriptorpb.FieldDescriptorProto{ Field: []*descriptorpb.FieldDescriptorProto{
{Name: proto.String("search"), Number: proto.Int32(1), Label: descriptorpb.FieldDescriptorProto_LABEL_OPTIONAL.Enum(), Type: descriptorpb.FieldDescriptorProto_TYPE_STRING.Enum()}, {Name: new("search"), Number: proto.Int32(1), Label: descriptorpb.FieldDescriptorProto_LABEL_OPTIONAL.Enum(), Type: descriptorpb.FieldDescriptorProto_TYPE_STRING.Enum()},
{Name: proto.String("page"), Number: proto.Int32(2), Label: descriptorpb.FieldDescriptorProto_LABEL_OPTIONAL.Enum(), Type: descriptorpb.FieldDescriptorProto_TYPE_INT32.Enum()}, {Name: new("page"), Number: proto.Int32(2), Label: descriptorpb.FieldDescriptorProto_LABEL_OPTIONAL.Enum(), Type: descriptorpb.FieldDescriptorProto_TYPE_INT32.Enum()},
{Name: proto.String("published_after"), Number: proto.Int32(3), Label: descriptorpb.FieldDescriptorProto_LABEL_OPTIONAL.Enum(), Type: descriptorpb.FieldDescriptorProto_TYPE_MESSAGE.Enum(), TypeName: proto.String(".google.protobuf.Timestamp")}, {Name: new("published_after"), Number: proto.Int32(3), Label: descriptorpb.FieldDescriptorProto_LABEL_OPTIONAL.Enum(), Type: descriptorpb.FieldDescriptorProto_TYPE_MESSAGE.Enum(), TypeName: new(".google.protobuf.Timestamp")},
}, },
}, { }, {
Name: proto.String("ListArticlesResponse"), Name: new("ListArticlesResponse"),
}}, }},
Service: []*descriptorpb.ServiceDescriptorProto{{ Service: []*descriptorpb.ServiceDescriptorProto{{
Name: proto.String("WikiService"), Name: new("WikiService"),
Method: []*descriptorpb.MethodDescriptorProto{{ Method: []*descriptorpb.MethodDescriptorProto{{
Name: proto.String("ListArticles"), InputType: proto.String(".mcpfixture.v1.ListArticlesRequest"), OutputType: proto.String(".mcpfixture.v1.ListArticlesResponse"), Name: new("ListArticles"), InputType: new(".mcpfixture.v1.ListArticlesRequest"), OutputType: new(".mcpfixture.v1.ListArticlesResponse"),
}, { }, {
Name: proto.String("StreamArticles"), InputType: proto.String(".mcpfixture.v1.ListArticlesRequest"), OutputType: proto.String(".mcpfixture.v1.ListArticlesResponse"), ServerStreaming: proto.Bool(true), Name: new("StreamArticles"), InputType: new(".mcpfixture.v1.ListArticlesRequest"), OutputType: new(".mcpfixture.v1.ListArticlesResponse"), ServerStreaming: new(true),
}}, }},
}}, }},
SourceCodeInfo: &descriptorpb.SourceCodeInfo{Location: []*descriptorpb.SourceCodeInfo_Location{{ SourceCodeInfo: &descriptorpb.SourceCodeInfo{Location: []*descriptorpb.SourceCodeInfo_Location{{
Path: []int32{6, 0, 2, 0}, Span: []int32{1, 0, 1, 1}, LeadingComments: proto.String("ListArticles lists Wiki articles.\n\nSupports pagination."), Path: []int32{6, 0, 2, 0}, Span: []int32{1, 0, 1, 1}, LeadingComments: new("ListArticles lists Wiki articles.\n\nSupports pagination."),
}}}, }}},
}, protoregistry.GlobalFiles) }, protoregistry.GlobalFiles)
if err != nil { if err != nil {

View File

@ -8,6 +8,7 @@ import (
abiv1 "git.dev.alexdunmow.com/block/pluginsdk/abi/v1" abiv1 "git.dev.alexdunmow.com/block/pluginsdk/abi/v1"
"git.dev.alexdunmow.com/block/pluginsdk/blocks" "git.dev.alexdunmow.com/block/pluginsdk/blocks"
"git.dev.alexdunmow.com/block/pluginsdk/document"
"git.dev.alexdunmow.com/block/pluginsdk/plugin" "git.dev.alexdunmow.com/block/pluginsdk/plugin"
"git.dev.alexdunmow.com/block/pluginsdk/templates" "git.dev.alexdunmow.com/block/pluginsdk/templates"
"google.golang.org/protobuf/proto" "google.golang.org/protobuf/proto"
@ -25,7 +26,20 @@ func tagFilterRegistration() plugin.PluginRegistration {
func(ctx context.Context, content map[string]any) string { func(ctx context.Context, content map[string]any) string {
title, _ := content["title"].(string) title, _ := content["title"].(string)
// Powered: return template + data; the host renders it. // Powered: return template + data; the host renders it.
return blocks.PoweredBlock("<h1>{{ title }}</h1>", map[string]any{"title": title}) return blocks.PoweredBlockWithOptions(
"<h1>{{ title }}</h1>",
map[string]any{"title": title},
blocks.PoweredOptions{
StatusCode: 404,
Metadata: &document.Metadata{
Title: "Missing",
Description: "Missing article",
CanonicalURL: "https://example.com/missing",
ImageURL: "https://example.com/missing.png",
Robots: "noindex,nofollow",
},
},
)
}) })
blocks.RegisterTag("greet", func(args map[string]any, rctx blocks.RenderContext) (string, error) { blocks.RegisterTag("greet", func(args map[string]any, rctx blocks.RenderContext) (string, error) {
who, _ := args["who"].(string) who, _ := args["who"].(string)
@ -111,6 +125,17 @@ func TestPoweredBlockRoundTrip(t *testing.T) {
if !strings.Contains(string(p.GetDataJson()), `"title":"Welcome"`) { if !strings.Contains(string(p.GetDataJson()), `"title":"Welcome"`) {
t.Errorf("data_json = %s", p.GetDataJson()) t.Errorf("data_json = %s", p.GetDataJson())
} }
if p.GetStatusCode() != 404 {
t.Errorf("status_code = %d, want 404", p.GetStatusCode())
}
metadata := p.GetMetadata()
if metadata == nil || metadata.GetTitle() != "Missing" ||
metadata.GetDescription() != "Missing article" ||
metadata.GetCanonicalUrl() != "https://example.com/missing" ||
metadata.GetImageUrl() != "https://example.com/missing.png" ||
metadata.GetRobots() != "noindex,nofollow" {
t.Errorf("metadata = %+v", metadata)
}
} }
func TestRenderTagRoundTrip(t *testing.T) { func TestRenderTagRoundTrip(t *testing.T) {

View File

@ -10,6 +10,7 @@ import (
"strings" "strings"
"git.dev.alexdunmow.com/block/pluginsdk/blocks" "git.dev.alexdunmow.com/block/pluginsdk/blocks"
"git.dev.alexdunmow.com/block/pluginsdk/document"
"git.dev.alexdunmow.com/block/pluginsdk/plugin" "git.dev.alexdunmow.com/block/pluginsdk/plugin"
"git.dev.alexdunmow.com/block/pluginsdk/templates" "git.dev.alexdunmow.com/block/pluginsdk/templates"
) )
@ -45,7 +46,20 @@ var Registration = plugin.PluginRegistration{
if powered, _ := content["powered"].(bool); powered { if powered, _ := content["powered"].(bool); powered {
// Powered path: hand the host a template + data instead of final // Powered path: hand the host a template + data instead of final
// HTML, so the host renders it (pongo2) after this call returns. // HTML, so the host renders it (pongo2) after this call returns.
return blocks.PoweredBlock("<p>hello {{ name }}</p>", map[string]any{"name": name}) return blocks.PoweredBlockWithOptions(
"<p>hello {{ name }}</p>",
map[string]any{"name": name},
blocks.PoweredOptions{
StatusCode: 404,
Metadata: &document.Metadata{
Title: "Missing greeting",
Description: "The greeting was not found.",
CanonicalURL: "https://example.com/greetings/missing",
ImageURL: "https://example.com/greetings/missing.png",
Robots: "noindex,nofollow",
},
},
)
} }
return fmt.Sprintf("<p>hello %s</p>", name) return fmt.Sprintf("<p>hello %s</p>", name)
}) })

View File

@ -241,6 +241,17 @@ func TestWasmFixtureTagFilterPoweredRoundTrip(t *testing.T) {
if !strings.Contains(string(p.GetDataJson()), `"name":"wazero"`) { if !strings.Contains(string(p.GetDataJson()), `"name":"wazero"`) {
t.Errorf("powered data_json = %s", p.GetDataJson()) t.Errorf("powered data_json = %s", p.GetDataJson())
} }
if p.GetStatusCode() != 404 {
t.Errorf("powered status_code = %d, want 404", p.GetStatusCode())
}
metadata := p.GetMetadata()
if metadata == nil || metadata.GetTitle() != "Missing greeting" ||
metadata.GetDescription() != "The greeting was not found." ||
metadata.GetCanonicalUrl() != "https://example.com/greetings/missing" ||
metadata.GetImageUrl() != "https://example.com/greetings/missing.png" ||
metadata.GetRobots() != "noindex,nofollow" {
t.Errorf("powered metadata = %+v", metadata)
}
// HOOK_RENDER_TAG dispatches to the registered tag fn (fresh invoke — // HOOK_RENDER_TAG dispatches to the registered tag fn (fresh invoke —
// no re-entrancy; RENDER_BLOCK already returned). // no re-entrancy; RENDER_BLOCK already returned).

View File

@ -5,6 +5,7 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"html" "html"
"net/url"
"strings" "strings"
"git.dev.alexdunmow.com/block/pluginsdk/blocks" "git.dev.alexdunmow.com/block/pluginsdk/blocks"
@ -350,7 +351,7 @@ func renderBlock(ctx context.Context, block map[string]any) string {
} }
img := fmt.Sprintf(`<img src="%s" alt="%s" />`, html.EscapeString(url), html.EscapeString(alt)) img := fmt.Sprintf(`<img src="%s" alt="%s" />`, html.EscapeString(url), html.EscapeString(alt))
if link != "" { if link = safeLinkURL(link); link != "" {
img = fmt.Sprintf(`<a href="%s">%s</a>`, html.EscapeString(link), img) img = fmt.Sprintf(`<a href="%s">%s</a>`, html.EscapeString(link), img)
} }
// The positioning wrapper only exists when a chip is rendered, so // The positioning wrapper only exists when a chip is rendered, so
@ -424,8 +425,8 @@ func renderBlock(ctx context.Context, block map[string]any) string {
name = url name = url
} }
sb.WriteString(`<div class="my-4 rounded border border-border p-4">`) sb.WriteString(`<div class="my-4 rounded border border-border p-4">`)
if url != "" { if href := safeLinkURL(url); href != "" {
fmt.Fprintf(&sb, `<a class="text-primary underline" href="%s">`, html.EscapeString(url)) fmt.Fprintf(&sb, `<a class="text-primary underline" href="%s">`, html.EscapeString(href))
sb.WriteString(html.EscapeString(name)) sb.WriteString(html.EscapeString(name))
sb.WriteString("</a>") sb.WriteString("</a>")
} else { } else {
@ -627,6 +628,7 @@ func renderInlineContent(content []map[string]any, insideLink bool) string {
case "link": case "link":
href, _ := itemMap["href"].(string) href, _ := itemMap["href"].(string)
href = safeLinkURL(href)
linkContent := inlineContentFromRaw(itemMap["content"]) linkContent := inlineContentFromRaw(itemMap["content"])
if href == "" { if href == "" {
sb.WriteString(renderInlineContent(linkContent, insideLink)) sb.WriteString(renderInlineContent(linkContent, insideLink))
@ -656,6 +658,30 @@ func renderInlineContent(content []map[string]any, insideLink bool) string {
return sb.String() return sb.String()
} }
// safeLinkURL accepts ordinary web, email, telephone, and relative links while
// rejecting active-content schemes such as javascript: and data:. Escaping a
// URL protects the HTML attribute boundary, but it does not make an unsafe URL
// scheme safe to navigate to.
func safeLinkURL(raw string) string {
trimmed := strings.TrimSpace(raw)
if trimmed == "" {
return ""
}
parsed, err := url.Parse(trimmed)
if err != nil || parsed.Scheme == "" {
if err != nil {
return ""
}
return trimmed
}
switch strings.ToLower(parsed.Scheme) {
case "http", "https", "mailto", "tel":
return trimmed
default:
return ""
}
}
// imageChipClass matches the page-builder image block's attribution chip // imageChipClass matches the page-builder image block's attribution chip
// (backend/blocks/tags via image.ninjatpl in the cms repo) so themes style // (backend/blocks/tags via image.ninjatpl in the cms repo) so themes style
// blog and page credits identically. // blog and page credits identically.
@ -700,6 +726,7 @@ func writeAttributionLink(b *strings.Builder, href, label string) {
// Trailing sentence punctuation (.,;:!?'") is excluded from the linked URL. // Trailing sentence punctuation (.,;:!?'") is excluded from the linked URL.
// Closing parens, brackets and braces are kept inside the URL only when // Closing parens, brackets and braces are kept inside the URL only when
// balanced with an opener inside the URL itself — so // balanced with an opener inside the URL itself — so
//
// "(see https://example.com)" links only "https://example.com" // "(see https://example.com)" links only "https://example.com"
// "https://en.wikipedia.org/wiki/Foo_(bar)" keeps the trailing paren. // "https://en.wikipedia.org/wiki/Foo_(bar)" keeps the trailing paren.
func autolinkText(text string) string { func autolinkText(text string) string {

View File

@ -183,6 +183,27 @@ func TestBlockNoteToHTML_NoNestedAnchorInsideExplicitLink(t *testing.T) {
} }
} }
func TestBlockNoteToHTML_RejectsActiveContentExplicitLink(t *testing.T) {
doc := map[string]any{
"blocks": []any{
map[string]any{
"type": "paragraph",
"content": []any{
map[string]any{
"type": "link",
"href": " javascript:alert(1) ",
"content": []any{map[string]any{"type": "text", "text": "read this"}},
},
},
},
},
}
html := BlockNoteToHTML(context.Background(), doc)
if html != "<p class=\"my-4\">read this</p>\n" {
t.Fatalf("unsafe link should render as plain text: %q", html)
}
}
func TestBlockNoteToHTML_NoAutolinkInsideCodeStyle(t *testing.T) { func TestBlockNoteToHTML_NoAutolinkInsideCodeStyle(t *testing.T) {
doc := map[string]any{ doc := map[string]any{
"blocks": []any{ "blocks": []any{

View File

@ -236,6 +236,28 @@ func TestFileBlock(t *testing.T) {
} }
} }
func TestFileBlockRejectsActiveContentURL(t *testing.T) {
doc := map[string]any{
"blocks": []any{
map[string]any{
"type": "file",
"props": map[string]any{
"url": "javascript:alert(document.domain)",
"name": "Unsafe link",
},
},
},
}
html := BlockNoteToHTML(context.Background(), doc)
if strings.Contains(html, "href=") || strings.Contains(html, "javascript:") {
t.Fatalf("unsafe file URL rendered as a link: %s", html)
}
if !strings.Contains(html, "Unsafe link") {
t.Fatalf("file label should remain visible as plain text: %s", html)
}
}
func TestFileBlockNoName(t *testing.T) { func TestFileBlockNoName(t *testing.T) {
doc := map[string]any{ doc := map[string]any{
"blocks": []any{ "blocks": []any{
@ -708,6 +730,18 @@ func TestImageBlockAltAndLink(t *testing.T) {
} }
} }
func TestImageBlockRejectsActiveContentLink(t *testing.T) {
html := BlockNoteToHTML(context.Background(), imageBlock(map[string]any{
"url": "/media/x.webp", "alt": "Sunset", "link": "JaVaScRiPt:alert(1)",
}))
if strings.Contains(html, "<a ") || strings.Contains(strings.ToLower(html), "javascript:") {
t.Fatalf("unsafe image link rendered: %s", html)
}
if !strings.Contains(html, `<img src="/media/x.webp"`) {
t.Fatalf("image should still render without its unsafe link: %s", html)
}
}
func TestImageBlockWidthAndAlign(t *testing.T) { func TestImageBlockWidthAndAlign(t *testing.T) {
html := BlockNoteToHTML(context.Background(), imageBlock(map[string]any{ html := BlockNoteToHTML(context.Background(), imageBlock(map[string]any{
"url": "/media/x.webp", "width": "medium", "align": "right", "url": "/media/x.webp", "width": "medium", "align": "right",

View File

@ -3,6 +3,8 @@ package templates
import ( import (
"context" "context"
"io" "io"
"git.dev.alexdunmow.com/block/pluginsdk/document"
) )
// PageDocument is a full-page template's contribution to the document. The // PageDocument is a full-page template's contribution to the document. The
@ -17,6 +19,8 @@ type PageDocument struct {
HTMLAttrs map[string]string // extra <html> attributes (e.g. data-theme) HTMLAttrs map[string]string // extra <html> attributes (e.g. data-theme)
HeadExtra HTMLComponent // appended inside <head> after bn.Head HeadExtra HTMLComponent // appended inside <head> after bn.Head
BodyEndExtra HTMLComponent // appended before </body> after bn.BodyEnd BodyEndExtra HTMLComponent // appended before </body> after bn.BodyEnd
StatusCode int32 // requested HTTP status; host accepts 200599
Metadata *document.Metadata
} }
func (p *PageDocument) Render(ctx context.Context, w io.Writer) error { func (p *PageDocument) Render(ctx context.Context, w io.Writer) error {