core/abi/proto/v1/invoke.proto
Alex Dunmow ad6d87bf06 feat(ninja): plugin build/verify → .bnp packer + ABI riders (WO-WZ-009)
`ninja plugin build` compiles a plugin to reactor-mode wasip1 wasm (Go >= 1.24
enforced), extracts manifest.pb by driving one HOOK_DESCRIBE over wazero with
failing host stubs, and packs a tar.zst .bnp (plugin.wasm, plugin.mod,
manifest.pb + migrations/schemas/assets/web-dist when present) with a summary
table. A describe-time capability call (e.g. db.* from Register) fails with an
actionable error naming the offending method. `ninja plugin verify` re-runs the
CMS reader's layout/name/abi/path-safety/size checks standalone (deliberate
duplication of cms backend/plugin/bnp/reader.go; kept in lockstep by WO-WZ-010).

ABI riders (additive; buf breaking clean):
- ABI_ERROR_CODE_TX_EXPIRED enum value + bnwasm guest mapping to a new
  bnwasm.ErrTxExpired sentinel (retryable tx expiry, distinct from real faults);
  the cms dbexec side adopts the emit separately.
- PluginManifest.data_dir bool + a first-class `data_dir` key on the plugin.mod
  parser (so writeMod's struct round-trip can't drop it); `plugin build` stamps
  it from plugin.mod into the manifest.

Docs: wasm-abi.md gains a Building & packing section, the error-code table row,
the manifest data_dir mapping, and the plugin.mod reference. Tests: CLI e2e
builds the WZ-002 fixture → verify + manifest block keys; a capfixture proves
the actionable describe-time error; verify rejects each malformed class;
bnwasm TX_EXPIRED classification.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 18:34:17 +08:00

196 lines
5.9 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;
}
// 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;
}