pongo2 stays host-side and is never compiled into a guest; plugins render
templates by handing the host a {template, data} pair, and the host calls
back into the free guest instance for plugin-declared tags/filters.
ABI (additive, buf-breaking clean):
- RenderBlockResponse gains a `powered` PoweredBlock{template, data_json};
a block returns EITHER html OR powered.
- New hooks HOOK_RENDER_TAG (10) / HOOK_APPLY_FILTER (11) with
RenderTag{Request,Response} and ApplyFilter{Request,Response}.
- PluginManifest gains repeated declared_tags / declared_filters (31/32).
Guest SDK (core/blocks, core/plugin/wasmguest):
- blocks.PoweredBlock(template, data) / DecodePoweredBlock: NUL-sentinel
marker so BlockFunc's string signature is unchanged (smallest additive
change — no ripple to existing blocks or the host guest-side).
- blocks.RegisterTag / RegisterFilter (+ RenderContext = context.Context)
write a package-level registry; runRegister resets it per registration
for deterministic DESCRIBE + dispatch.
- DESCRIBE emits declared_tags/filters; dispatch handles RENDER_TAG /
APPLY_FILTER (fn errors → response.error; panics → AbiError INTERNAL,
instance stays callable).
Docs: core/docs/wasm-abi.md gains the render-as-a-host-capability model,
the powered-block flow (re-entrancy-free), the plugin API, and the
HOST-SIDE CONTRACT the cms phase implements.
Tests: unit round-trips for powered/RENDER_TAG/APPLY_FILTER (dispatch +
error + unknown + panic + per-guest registry isolation) plus a real
wazero round-trip through the compiled fixture module. Verified no
guest-reachable package imports pongo2 (go list -deps on the wasip1
fixture build is clean).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
203 lines
6.2 KiB
Protocol Buffer
203 lines
6.2 KiB
Protocol Buffer
// invoke.proto — the bn_invoke envelope, hook catalog, and the job/lifecycle
|
|
// hook payloads (WO-WZ-001).
|
|
//
|
|
// Every host→guest call crosses the wasm boundary as one guest export:
|
|
//
|
|
// bn_invoke(hook_id, ptr, len) → packed(ptr, len)
|
|
//
|
|
// where (ptr, len) frames a serialized InvokeRequest and the packed return
|
|
// frames a serialized InvokeResponse. Hook-specific payloads (render.proto,
|
|
// http.proto, and the messages below) travel inside InvokeRequest.payload /
|
|
// InvokeResponse.payload. See core/docs/wasm-abi.md.
|
|
|
|
syntax = "proto3";
|
|
|
|
package abi.v1;
|
|
|
|
import "v1/manifest.proto";
|
|
|
|
option go_package = "git.dev.alexdunmow.com/block/core/abi/v1;abiv1";
|
|
|
|
// Hook identifies the guest entry point being invoked.
|
|
enum Hook {
|
|
HOOK_UNSPECIFIED = 0;
|
|
// Render one block: payload = RenderBlockRequest / RenderBlockResponse.
|
|
HOOK_RENDER_BLOCK = 1;
|
|
// Render one template: payload = RenderTemplateRequest / RenderTemplateResponse.
|
|
HOOK_RENDER_TEMPLATE = 2;
|
|
// Forward a buffered HTTP request to the guest's mux:
|
|
// payload = HttpRequest / HttpResponse.
|
|
HOOK_HANDLE_HTTP = 3;
|
|
// Run a background job handler: payload = JobRequest / JobResponse.
|
|
HOOK_JOB = 4;
|
|
// Plugin load lifecycle: payload = LoadRequest / LoadResponse.
|
|
HOOK_LOAD = 5;
|
|
// Plugin unload lifecycle: payload = UnloadRequest / UnloadResponse.
|
|
HOOK_UNLOAD = 6;
|
|
// Re-fetch content for RAG re-indexing:
|
|
// payload = RagFetchRequest / RagFetchResponse.
|
|
HOOK_RAG_FETCH = 7;
|
|
// Media lifecycle event delivery: payload = MediaHookRequest / MediaHookResponse.
|
|
HOOK_MEDIA_HOOK = 8;
|
|
// Capture the static manifest at publish time:
|
|
// payload = DescribeRequest / DescribeResponse.
|
|
HOOK_DESCRIBE = 9;
|
|
// Invoke a plugin-declared template tag (manifest.declared_tags) that the
|
|
// host engine hit while rendering a powered block:
|
|
// payload = RenderTagRequest / RenderTagResponse.
|
|
HOOK_RENDER_TAG = 10;
|
|
// Apply a plugin-declared template filter (manifest.declared_filters):
|
|
// payload = ApplyFilterRequest / ApplyFilterResponse.
|
|
HOOK_APPLY_FILTER = 11;
|
|
}
|
|
|
|
// InvokeRequest is the host→guest call envelope.
|
|
message InvokeRequest {
|
|
Hook hook = 1;
|
|
// Serialized hook-specific request message (see Hook value comments).
|
|
bytes payload = 2;
|
|
// Milliseconds the guest has to answer; the host also enforces this
|
|
// deadline on the wasm instance (WithCloseOnContextDone).
|
|
int64 deadline_ms = 3;
|
|
}
|
|
|
|
// InvokeResponse is the guest→host return envelope.
|
|
message InvokeResponse {
|
|
// Serialized hook-specific response message; empty when error is set.
|
|
bytes payload = 1;
|
|
// Set when the hook failed; the host treats decode failures and traps as
|
|
// implicit ABI_ERROR_CODE_INTERNAL.
|
|
AbiError error = 2;
|
|
}
|
|
|
|
// AbiErrorCode classifies boundary-crossing failures.
|
|
enum AbiErrorCode {
|
|
ABI_ERROR_CODE_UNSPECIFIED = 0;
|
|
// The handler ran and failed; message carries the Go error text.
|
|
ABI_ERROR_CODE_INTERNAL = 1;
|
|
// The payload could not be decoded.
|
|
ABI_ERROR_CODE_DECODE = 2;
|
|
// The hook/capability is not implemented by the callee.
|
|
ABI_ERROR_CODE_UNIMPLEMENTED = 3;
|
|
// The call exceeded its deadline; the instance is considered poisoned.
|
|
ABI_ERROR_CODE_DEADLINE_EXCEEDED = 4;
|
|
// The caller is not entitled to this capability.
|
|
ABI_ERROR_CODE_PERMISSION_DENIED = 5;
|
|
// A db.* call named a transaction handle the host has already expired
|
|
// (dropped at the call-chain deadline, WO-WZ-007). Distinct from a real
|
|
// fault: the unit of work is retryable in a fresh transaction. Emitted by
|
|
// the cms dbexec side (adopted separately) and mapped guest-side to
|
|
// bnwasm.ErrTxExpired.
|
|
ABI_ERROR_CODE_TX_EXPIRED = 6;
|
|
}
|
|
|
|
// AbiError is the structured error carried by InvokeResponse and
|
|
// HostCallResponse.
|
|
message AbiError {
|
|
AbiErrorCode code = 1;
|
|
string message = 2;
|
|
}
|
|
|
|
// --- DESCRIBE ---
|
|
|
|
// DescribeRequest asks the guest for its static manifest (publish time only).
|
|
message DescribeRequest {
|
|
// ABI major version of the calling host/publish tool.
|
|
uint32 host_abi_version = 1;
|
|
}
|
|
|
|
// DescribeResponse returns the static manifest.
|
|
message DescribeResponse {
|
|
PluginManifest manifest = 1;
|
|
}
|
|
|
|
// --- LOAD / UNLOAD ---
|
|
|
|
// LoadRequest carries the static host configuration the .so world exposed as
|
|
// CoreServices.AppURL / CoreServices.MediaPath.
|
|
message LoadRequest {
|
|
HostConfig host_config = 1;
|
|
}
|
|
|
|
// HostConfig mirrors the plain-value CoreServices fields.
|
|
message HostConfig {
|
|
string app_url = 1; // CoreServices.AppURL
|
|
string media_path = 2; // CoreServices.MediaPath
|
|
}
|
|
|
|
message LoadResponse {}
|
|
|
|
message UnloadRequest {}
|
|
|
|
message UnloadResponse {}
|
|
|
|
// --- JOB ---
|
|
|
|
// JobRequest dispatches one background job to the guest handler registered
|
|
// for job_type (manifest.job_types).
|
|
message JobRequest {
|
|
string job_type = 1;
|
|
// JSON job configuration (json.RawMessage in JobHandlerFunc).
|
|
bytes config_json = 2;
|
|
}
|
|
|
|
// JobResponse returns the handler's JSON result.
|
|
message JobResponse {
|
|
bytes result_json = 1;
|
|
}
|
|
|
|
// --- RAG_FETCH ---
|
|
|
|
// RagFetchRequest asks the guest's registered content fetcher
|
|
// (manifest.rag_content_fetcher_types) for a content item's text.
|
|
message RagFetchRequest {
|
|
string content_type = 1;
|
|
string content_id = 2; // UUID
|
|
}
|
|
|
|
// RagFetchResponse mirrors plugin.ContentFetcher's return values.
|
|
message RagFetchResponse {
|
|
string title = 1;
|
|
string text = 2;
|
|
}
|
|
|
|
// --- MEDIA_HOOK ---
|
|
|
|
// MediaHookRequest delivers one media lifecycle event
|
|
// (plugin.MediaHooksProvider).
|
|
message MediaHookRequest {
|
|
oneof event {
|
|
MediaAnalyzedEvent media_analyzed = 1; // OnMediaAnalyzed
|
|
ModerationDecisionEvent moderation_decision = 2; // OnModerationDecision
|
|
}
|
|
}
|
|
|
|
message MediaHookResponse {}
|
|
|
|
// MediaAnalyzedEvent mirrors plugin.MediaAnalyzedEvent (UUIDs as strings).
|
|
message MediaAnalyzedEvent {
|
|
string media_id = 1;
|
|
string analysis_id = 2;
|
|
string content_hash = 3;
|
|
string status = 4;
|
|
string source_plugin = 5;
|
|
string source_type = 6;
|
|
string source_ref_id = 7;
|
|
string safe_adult = 8;
|
|
string safe_violence = 9;
|
|
string safe_racy = 10;
|
|
}
|
|
|
|
// ModerationDecisionEvent mirrors plugin.ModerationDecisionEvent.
|
|
message ModerationDecisionEvent {
|
|
string media_id = 1;
|
|
string analysis_id = 2;
|
|
string status = 3;
|
|
string previous_status = 4;
|
|
string source_plugin = 5;
|
|
string source_type = 6;
|
|
string source_ref_id = 7;
|
|
string moderated_by = 8;
|
|
string note = 9;
|
|
}
|