Bootstrap the plugin-facing SDK module so the fleet migration (P2) becomes a mechanical block/core/X -> block/pluginsdk/X import rewrite. - abi/proto/v1: the single source-of-truth ABI proto tree, copied from the cms authoring source (cms/backend/abi/proto/v1) with go_package retargeted to git.dev.alexdunmow.com/block/pluginsdk/abi/v1;abiv1. Kills the hand-kept cms/core proto duplication (audit gap 4). - abi/v1: generated Go bindings (buf generate); byte-identical to core's abi/v1 save the embedded go_package path. - plugin/ (registration + DI surface), plugin/wasmguest/** (transport shim, caps stubs, bnwasm db driver, testdata fixtures + golden .pb), and the guest-facing type packages: blocks (+builtin/shared/tags), templates (+pongo/bn), auth, settings, content, gating, crypto, rbac, video, ai, subscriptions, menus, datasources. Internal imports rewritten core -> pluginsdk; zero block/core references remain. - README/AGENTS(+CLAUDE symlink)/Makefile: proto is the contract, Go is one binding; no replace directives; templates/bn is a synced copy authored in cms. Spec: cms docs/superpowers/specs/2026-07-07-proto-first-plugin-sdk-design.md Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
99 lines
3.4 KiB
Go
99 lines
3.4 KiB
Go
//go:build wasip1
|
|
|
|
package wasmguest
|
|
|
|
import (
|
|
abiv1 "git.dev.alexdunmow.com/block/pluginsdk/abi/v1"
|
|
"unsafe"
|
|
)
|
|
|
|
// pinned keeps host-visible buffers reachable so the Go GC never frees (or,
|
|
// under a future moving GC, relocates) memory the host holds a raw pointer
|
|
// into. Keyed by the buffer's linear-memory address. Wasm instances are
|
|
// single-threaded per the ABI (one bn_invoke at a time), so no locking.
|
|
var pinned = make(map[uint32][]byte)
|
|
|
|
// lastResponse pins the most recent bn_invoke response buffer; per the ABI
|
|
// it stays valid until the next bn_invoke on this instance.
|
|
var lastResponse []byte
|
|
|
|
// bn_alloc allocates size bytes of guest memory for the host to write into.
|
|
// The region stays valid until the current bn_invoke call returns (request
|
|
// buffers) or until released with bn_free / consumed by the shim (host-call
|
|
// responses).
|
|
//
|
|
//go:wasmexport bn_alloc
|
|
func bnAlloc(size uint32) uint32 {
|
|
if size == 0 {
|
|
return 0
|
|
}
|
|
buf := make([]byte, size)
|
|
ptr := bufPtr(buf)
|
|
pinned[ptr] = buf
|
|
return ptr
|
|
}
|
|
|
|
// bn_free releases a buffer previously returned by bn_alloc. Unknown
|
|
// pointers are ignored (the shim may have already released the buffer).
|
|
//
|
|
//go:wasmexport bn_free
|
|
func bnFree(ptr uint32) {
|
|
delete(pinned, ptr)
|
|
}
|
|
|
|
// bn_invoke dispatches one hook call. (ptr, len) frames a serialized
|
|
// InvokeRequest written by the host into bn_alloc'd memory; the packed
|
|
// return value ((ptr << 32) | len) frames a serialized InvokeResponse valid
|
|
// until the next bn_invoke. A return of 0 means the guest could not even
|
|
// produce an error envelope; the host treats it as INTERNAL and discards
|
|
// the instance.
|
|
//
|
|
//go:wasmexport bn_invoke
|
|
func bnInvoke(hookID uint32, ptr uint32, length uint32) uint64 {
|
|
var (
|
|
req []byte
|
|
known bool
|
|
)
|
|
if buf, ok := pinned[ptr]; ok && uint32(len(buf)) >= length {
|
|
req = buf[:length:length]
|
|
known = true
|
|
}
|
|
|
|
var resp []byte
|
|
switch {
|
|
case !known:
|
|
// The ABI requires request buffers to come from bn_alloc; anything
|
|
// else is a protocol violation, not readable guest memory.
|
|
resp = errorResponse(abiv1.AbiErrorCode_ABI_ERROR_CODE_DECODE,
|
|
"bn_invoke request pointer was not allocated via bn_alloc")
|
|
case current == nil:
|
|
resp = errorResponse(abiv1.AbiErrorCode_ABI_ERROR_CODE_INTERNAL,
|
|
"wasmguest.Serve was never called — plugin main package is missing the init boilerplate (see wasmguest doc.go)")
|
|
default:
|
|
resp = current.invoke(hookID, req)
|
|
}
|
|
|
|
// Request buffer ownership ends with this call (ABI: valid until
|
|
// bn_invoke returns). Release it whether or not the host calls bn_free.
|
|
delete(pinned, ptr)
|
|
|
|
// A logically-empty InvokeResponse (Error nil + empty payload — e.g.
|
|
// LoadResponse/UnloadResponse) proto-marshals to ZERO bytes. That is a
|
|
// valid success envelope, not the "couldn't produce an envelope" sentinel,
|
|
// so frame it as (ptr, 0) with a real pointer. Returning packed 0 here
|
|
// would make the host mistake every empty-response hook (notably a
|
|
// successful LOAD) for an INTERNAL failure and discard the instance. A
|
|
// 1-byte backing keeps the pointer valid while the reported length stays 0
|
|
// (the host reads zero bytes → an empty InvokeResponse, which is correct).
|
|
if len(resp) == 0 {
|
|
lastResponse = []byte{0}
|
|
return uint64(bufPtr(lastResponse)) << 32
|
|
}
|
|
lastResponse = resp
|
|
return uint64(bufPtr(resp))<<32 | uint64(uint32(len(resp)))
|
|
}
|
|
|
|
func bufPtr(b []byte) uint32 {
|
|
return uint32(uintptr(unsafe.Pointer(&b[0])))
|
|
}
|