From 925b6051a3c3d58e83e069a6c0f4f0283cd3d73f Mon Sep 17 00:00:00 2001 From: Alex Dunmow Date: Sat, 4 Jul 2026 10:28:44 +0800 Subject: [PATCH] docs(abi): capability-surface matrix + wasm-abi.md updated for WO-WZ-019 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit abi-capability-surface.md is the canonical plugin-need → ABI-mechanism map (manifest / host_call / hook), with WZ-020 rows marked. wasm-abi.md: content family drift fixed (list_posts), new families and hooks documented, open items closed except slot/media/embed render resolvers. Co-Authored-By: Claude Fable 5 --- docs/abi-capability-surface.md | 99 ++++++++++++++++++++++++++++++++++ docs/wasm-abi.md | 53 +++++++++++------- 2 files changed, 132 insertions(+), 20 deletions(-) create mode 100644 docs/abi-capability-surface.md diff --git a/docs/abi-capability-surface.md b/docs/abi-capability-surface.md new file mode 100644 index 0000000..7fcf578 --- /dev/null +++ b/docs/abi-capability-surface.md @@ -0,0 +1,99 @@ +# ABI Capability Surface (WO-WZ-019) + +The canonical map of **every capability a plugin needs from the CMS** to its +ABI mechanism. The rule is *injection, not reliance*: a plugin obtains 100% of +its host interactions by (a) declaring static intent in `manifest.pb`, (b) +calling a `blockninja.host_call` capability method, or (c) implementing a +host-invoked hook. Nothing requires linking the Go `core` module — the Go +guest SDK (`plugin/wasmguest`) is an ergonomic front-end over these +primitives, not part of the model. A plugin in any language that speaks +protobuf over the [wasm ABI](wasm-abi.md) reaches everything below. + +Legend — **Mechanism**: `manifest` (static declaration, consumed by the host +loader), `host_call` (dynamic guest→host capability), `hook` (host-invoked +guest logic), `artifact` (a directory in the `.bnp` the host consumes). +**Codeless equivalent**: how a plugin with NO `plugin.wasm` gets the same +effect (the WO-WZ-020 declarative path; rows marked *WZ-020* land there). + +## Static contributions (what the plugin IS) + +| Capability | Mechanism | Status | Codeless equivalent | +|---|---|---|---| +| Register block types (key/title/category/icon/schema) | `manifest.blocks` (`BlockMeta`) + `schemas/` artifact | ABI-native | Same — pure declaration, zero code | +| Block template overrides | `manifest.block_template_overrides` | ABI-native | Same | +| Templates / system templates / page templates | `manifest.template_keys`, `system_templates`, `page_templates` | ABI-native | Same + `.ninjatpl` files in the artifact (WZ-020) | +| Master pages | `manifest.master_pages` | ABI-native | Same | +| Email wrappers | `manifest.email_wrapper_system_keys` | ABI-native | Same | +| Template tags / filters (computing) | `manifest.declared_tags` / `declared_filters` + `HOOK_RENDER_TAG` / `HOOK_APPLY_FILTER` | ABI-native | N/A — a *computing* tag requires code by definition | +| Template tags (pure snippet/partial, no logic) | today: same as computing (hook) | **WZ-020**: declare as a template partial, host-rendered, no hook | Partial file in `templates/` | +| Powered block with a *declared data provider* (no render hook at all) | today: `HOOK_RENDER_BLOCK` returning `RenderBlockResponse.powered` | **WZ-020**: manifest provider declaration (`posts`, `pages`, `menu`, `datasource:`, …) + `.ninjatpl`; host fetches + renders | The WZ-020 headline feature | +| Settings schema / settings panel / admin pages | `manifest.settings_schema`, `settings_panel`, `admin_pages` | ABI-native | Same | +| Theme presets / bundled fonts / CSS manifest / icon packs | `manifest.theme_presets`, `bundled_fonts`, `css_manifest`, `required_icon_packs` | ABI-native (JSON payloads; no Go types on the wire) | Same. Fonts are **purely declarative** — the host consumes `bundled_fonts` at load; runtime *activation* (choosing a font) is a site setting, writable via `settings.update_site_setting`. | +| AI actions | `manifest.ai_actions` | ABI-native | Same | +| RBAC method roles / core service bindings | `manifest.rbac_method_roles`, `core_service_bindings` | ABI-native | Same (bindings host-mounted) | +| Job types / RAG fetcher types / directory extensions (static fields) | `manifest.job_types`, `rag_content_fetcher_types`, `directory_extensions` | ABI-native | Job/RAG types need their hooks → code | +| Migrations / assets / MF web bundle | `artifact` `migrations/`, `assets/`, `web/` (host-run/served) | ABI-native | Same | +| Persistent data dir | `plugin.mod data_dir` → `manifest.data_dir` | ABI-native | N/A (no code to need it) | + +## Dynamic requests (host_call families) + +Every family below is registered in the cms host (`plugin/wasmhost/caps`) and +mirrored 1:1 by a guest SDK stub. Wire types are language-neutral: canonical +UUID strings, JSON bytes for maps, protobuf for structure. + +| Capability | Methods | Status | +|---|---|---| +| Content reads | `content.get_author_profile`, `get_page`, `get_post`, `list_posts`, `slugify`, `block_note_to_html`, `generate_excerpt`, `strip_html` | ABI-native | +| **Content authoring** (WZ-019) | `content.create_page`, `set_page_blocks`, `publish_page`, `set_page_seo`, `upsert_post` | ABI-native | +| **Provisioning** (WZ-019) — idempotent seed/ensure, **Load-time** | `provisioner.ensure_data_table`, `merge_site_settings`, `ensure_setting`, `ensure_page`, `override_site_settings`, `ensure_menu_item`, `register_embedding_config`, `ensure_embed`, `ensure_job_schedule`, `update_data_table_row_field`, `disable_orphaned_job_schedules`, `ensure_plugin`, `ensure_custom_color`, `ensure_media` | ABI-native. `RegisterWithProvisioner` is **inert under wasm** (DESCRIBE stubs host functions): call `deps.Provisioner` from `Load` instead. `EnsureEmbed` crosses template-rendered embeds only — `RenderFunc` cannot serialize. | +| Settings | `settings.get_site_settings`, `get_plugin_settings`, `update_site_setting`, **`update_plugin_settings`** (WZ-019; host-pinned to the caller's own plugin) | ABI-native | +| Menus (read) | `menus.get_menu_by_name`, `get_menu_items` | ABI-native. Menu *writes* are seed-shaped → `provisioner.ensure_menu_item`. | +| Media | `media.deposit` (full pipeline, deterministic-ID + `created` flag, folder, alt text, stable `media:` ref) + `provisioner.ensure_media` (immutable seed semantics) | ABI-native | +| Gating / crypto / datasources / users / subscriptions | as in [wasm-abi.md](wasm-abi.md) §Capability calls | ABI-native | +| Email | `email.send` | ABI-native | +| AI | `ai.text_call`, `ai.tools.register` (+ execution via `HOOK_AI_TOOL_CALL`) | ABI-native | +| Jobs | `jobs.submit`, **`jobs.progress`** (WZ-019; correlated to the running job via the HOOK_JOB call context — no job ID on the wire) | ABI-native | +| Bridge | `bridge.register_service`, `get_service` (availability), **`bridge.invoke`** (WZ-019; opaque-payload cross-plugin calls → provider's `BridgeInvokable` or `HOOK_BRIDGE_CALL`) | ABI-native. Typed in-process Go values still don't cross — by design; `GetService` stays nil for wasm consumers. | +| Embeddings / RAG / reviews / badges | as in wasm-abi.md | ABI-native | +| Database | `db.query/exec/tx_*` under the per-plugin Postgres role | ABI-native | + +## Host-invoked hooks (logic the plugin injects) + +| Hook | Contract | Status | +|---|---|---| +| `HOOK_DESCRIBE` | `DescribeRequest/Response` — manifest capture at publish | ABI-native | +| `HOOK_LOAD` / `HOOK_UNLOAD` | `LoadRequest` (host config) / `UnloadRequest` | ABI-native | +| `HOOK_RENDER_BLOCK` | html OR powered `{template, data_json}` | ABI-native | +| `HOOK_RENDER_TEMPLATE` | `RenderTemplateRequest/Response` | ABI-native | +| `HOOK_RENDER_TAG` / `HOOK_APPLY_FILTER` | declared-tag/filter callbacks, re-entrancy-free | ABI-native | +| `HOOK_HANDLE_HTTP` | buffered HTTP + trusted identity headers | ABI-native | +| `HOOK_JOB` | `JobRequest/Response` + `jobs.progress` | ABI-native | +| `HOOK_RAG_FETCH` | content re-index callback | ABI-native | +| `HOOK_MEDIA_HOOK` | media lifecycle events | ABI-native | +| **`HOOK_AI_TOOL_CALL`** (WZ-019) | `AiToolCallRequest{slug, params_json}` → `{content, error_message}`; executes the guest handler recorded at `ai.tools.register` | ABI-native | +| **`HOOK_BRIDGE_CALL`** (WZ-019) | `BridgeCallRequest{service_name, method, payload}` → `{payload}`; provider side of `bridge.invoke` | ABI-native | +| **`HOOK_DIRECTORY_PANEL_SECTION`** / **`HOOK_DIRECTORY_PIN_DECORATOR`** (WZ-019) | indexed callbacks over `manifest.directory_extensions` counts; pin decorators return the mutated pin | ABI-native | + +**Per-instance rule for callback hooks:** anything the host may call back on +an arbitrary pooled instance (AI tools, bridge services, RAG fetchers) must be +registered in `Register` — which runs on every instance — not in `Load` +(one instance only). Use `wasmguest.HostServices()` for deps at Register time. + +## Remaining open items + +- **Slot / media / embed resolvers during a guest render** — `RenderContext` + still cannot carry them (function values). The powered-block path sidesteps + this: host-side rendering has the real resolvers. Only a plain-HTML guest + block that wants to resolve `media:` refs itself is affected. Tracked for a + render-focused WO; not blocking any shipping plugin. + +## Conformance + +- Guest↔host wire parity: every capability method has a golden + request/response pair (`plugin/wasmguest/caps/testdata/golden/`, generated + by the guest stubs) that the cms host replays byte-for-byte through its real + handlers (`cms plugin/wasmhost/caps` `TestGoldenParity`). The count check + fails if a registered method lacks a golden. +- Injection-not-reliance: `TestRawProtobufConformance` (cms caps package) + drives representative capabilities with hand-constructed protobuf messages + through the `HostCallRequest` envelope — no guest SDK in the path. diff --git a/docs/wasm-abi.md b/docs/wasm-abi.md index 885a232..f25733c 100644 --- a/docs/wasm-abi.md +++ b/docs/wasm-abi.md @@ -179,6 +179,11 @@ init lazily and only on instances that serve those hooks. A block render (`HOOK_RENDER_BLOCK`) receives **no** services — only `ctx` + content — so a DB-backed block cannot get its pool from Load. +The same rule governs the WO-WZ-019 callback hooks: AI tool handlers +(`ai.tools.register`) and bridge service values (`Bridge.RegisterService`) +are recorded per instance, and `HOOK_AI_TOOL_CALL` / `HOOK_BRIDGE_CALL` may +land on ANY pooled instance — so register them in `Register`, not `Load`. + The escape hatch is `wasmguest.HostServices()`: it returns the `CoreServices` bound at `_initialize` on **every** instance (live `db.*` `Pool` + capability stubs). A DB-backed block registers its pool from `HostServices().Pool` inside @@ -240,6 +245,10 @@ Host→guest calls. `InvokeRequest{hook, payload, deadline_ms}` → | `HOOK_RAG_FETCH` | `RagFetchRequest` / `RagFetchResponse` | RAG re-index callback for a `manifest.rag_content_fetcher_types` entry (`plugin.ContentFetcher`). | | `HOOK_MEDIA_HOOK` | `MediaHookRequest` / `MediaHookResponse` | Media lifecycle event (`plugin.MediaHooksProvider`), only when `manifest.has_media_hooks`. | | `HOOK_DESCRIBE` | `DescribeRequest` / `DescribeResponse` | Publish-time manifest capture; the result is stored as `manifest.pb` in the `.bnp` artifact. Never called on a live instance. | +| `HOOK_AI_TOOL_CALL` | `AiToolCallRequest` / `AiToolCallResponse` | Execute the guest handler of a tool registered via `ai.tools.register`. Register tools in `Register` (every pooled instance runs it), not `Load`. | +| `HOOK_BRIDGE_CALL` | `BridgeCallRequest` / `BridgeCallResponse` | Provider side of `bridge.invoke`: run a method on one of THIS plugin's registered bridge services (`plugin.BridgeInvokable`). Register services in `Register`. | +| `HOOK_DIRECTORY_PANEL_SECTION` | `DirectoryPanelSectionRequest` / `DirectoryPanelSectionResponse` | Render the index-th `DirectoryExtensions.PanelSections` callback. | +| `HOOK_DIRECTORY_PIN_DECORATOR` | `DirectoryPinDecoratorRequest` / `DirectoryPinDecoratorResponse` | Run the index-th pin decorator; the mutated pin map is returned. | ## Capability calls (`capability.proto`, `db.proto`) @@ -250,8 +259,8 @@ each pair mirrors one Go interface method from `CoreServices` 1:1 | Family | Methods | Go surface | |---|---|---| -| `content` | `get_author_profile`, `get_page`, `get_post`, `slugify`, `block_note_to_html`, `generate_excerpt`, `strip_html` | `content.Content` | -| `settings` | `get_site_settings`, `get_plugin_settings`, `update_site_setting` | `settings.Settings`, `settings.Updater` | +| `content` | `get_author_profile`, `get_page`, `get_post`, `list_posts`, `slugify`, `block_note_to_html`, `generate_excerpt`, `strip_html` — plus the WO-WZ-019 writes: `create_page`, `set_page_blocks`, `publish_page`, `set_page_seo`, `upsert_post` | `content.Content`, `content.Author` | +| `settings` | `get_site_settings`, `get_plugin_settings`, `update_site_setting`, `update_plugin_settings` (host-pinned to the caller's own plugin) | `settings.Settings`, `settings.Updater` | | `gating` | `get_subscriber_tier_level`, `evaluate_access` | `gating.Gating` | | `crypto` | `encrypt_secret`, `decrypt_secret` | `crypto.Crypto` | | `menus` | `get_menu_by_name`, `get_menu_items` | `menus.Menus` | @@ -261,8 +270,9 @@ each pair mirrors one Go interface method from `CoreServices` 1:1 | `media` | `deposit` | `plugin.Media` | | `email` | `send` | `plugin.EmailSender` | | `ai` | `text_call`, `tools.register` | `CoreServices.AITextCall`, `ai.ToolRegistry` | -| `bridge` | `register_service`, `get_service` | `plugin.PluginBridge` | -| `jobs` | `submit` | `plugin.JobRunner` | +| `bridge` | `register_service`, `get_service`, `invoke` (opaque-payload cross-plugin calls; provider answers via `HOOK_BRIDGE_CALL` or an in-process `plugin.BridgeInvokable`) | `plugin.PluginBridge` | +| `jobs` | `submit`, `progress` (correlated to the running job via the HOOK_JOB call context) | `plugin.JobRunner` + `JobHandlerFunc`'s progress callback | +| `provisioner` | `ensure_data_table`, `merge_site_settings`, `ensure_setting`, `ensure_page`, `override_site_settings`, `ensure_menu_item`, `register_embedding_config`, `ensure_embed`, `ensure_job_schedule`, `update_data_table_row_field`, `disable_orphaned_job_schedules`, `ensure_plugin`, `ensure_custom_color`, `ensure_media` — **Load-time**: call `deps.Provisioner` from `Load`; `RegisterWithProvisioner` is inert under wasm (DESCRIBE stubs host functions). `EnsureEmbed` rejects `RenderFunc`-only embeds (functions cannot cross). | `plugin.Provisioner` | | `embeddings` | `generate_embedding`, `embed_content`, `is_available` | `plugin.EmbeddingService` | | `rag` | `query`, `on_content_changed` | `plugin.RAGService` | | `reviews` | `submit_review` | `plugin.ReviewSubmitter` | @@ -282,7 +292,9 @@ No silent gaps: each member is either a guest stub or served host-side. | Member | Disposition | |---|---| -| `Content` (7 methods) | **stub** — `caps/content.go` | +| `Content` (8 methods) | **stub** — `caps/content.go` | +| `ContentAuthor` (5 write methods, WO-WZ-019) | **stub** — `caps/author.go` | +| `Provisioner` (14 methods, WO-WZ-019) | **stub** — `caps/provisioner.go`; Load-time only (see the `provisioner` family row) | | `Settings` / `SettingsUpdater` | **stub** — `caps/settings.go` (one value, both fields) | | `Gating` | **stub** — `caps/gating.go`; `EvaluateAccess` crosses but falls back to the pure `gating.EvaluateAccess` on transport error | | `Crypto` | **stub** — `caps/crypto.go` | @@ -291,9 +303,9 @@ No silent gaps: each member is either a guest stub or served host-side. | `PublicUsers` | **stub** — `caps/users.go` | | `Subscriptions` | **stub** — `caps/subscriptions.go` | | `Media` | **stub** — `caps/media.go` | -| `ToolRegistry` + `AITextCall` | **stub** — `caps/ai.go` (`ai.tools.register` + `ai.text_call`; tool `Handler` stays guest-side) | +| `ToolRegistry` + `AITextCall` | **stub** — `caps/ai.go` (`ai.tools.register` + `ai.text_call`); the tool `Handler` stays guest-side, recorded per instance and executed via `HOOK_AI_TOOL_CALL` — register tools in `Register`, not `Load` | | `EmailSender` | **stub** — `caps/email.go` | -| `Bridge` | **stub** — `caps/bridge.go`; `RegisterService` forwards names only (value dropped), `GetService` reports availability but returns `nil` (a typed value cannot cross — open item) | +| `Bridge` | **stub** — `caps/bridge.go`; `RegisterService` forwards the name AND records the value locally for `HOOK_BRIDGE_CALL` dispatch; `GetService` reports availability but returns `nil` (a typed value cannot cross — by design); cross-plugin calls use `Invoke` (`bridge.invoke`, opaque payloads) | | `ReviewSubmitter` | **stub** — `caps/reviews.go` | | `BadgeRefresher` | **stub** — `caps/badges.go` | | `JobRunner` | **stub** — `caps/jobs.go` | @@ -571,19 +583,20 @@ The core half (this SDK) defines the wire + guest dispatch. The cms host must: > wasm world. This SDK ships the mechanism only; per-plugin migration (e.g. > assumechaos) is a later phase. -## Open items (flagged for runtime WOs — additive, no major bump needed) +## Open items + +WO-WZ-019 closed the original set: AI tool execution (`HOOK_AI_TOOL_CALL`), +job progress (`jobs.progress`), cross-plugin bridge invocation +(`bridge.invoke` + `HOOK_BRIDGE_CALL` + `plugin.BridgeInvokable`), directory +extension callbacks (`HOOK_DIRECTORY_PANEL_SECTION` / +`HOOK_DIRECTORY_PIN_DECORATOR`), plugin provisioning (the `provisioner.*` +family), content authoring (`content.*` writes), and the plugin-own settings +write. The full capability→mechanism map lives in +[abi-capability-surface.md](abi-capability-surface.md). + +Still open: -- **AI tool execution**: `AiToolRegisterRequest` registers a tool; executing - its guest-side `Handler` needs a host→guest hook (e.g. `HOOK_AI_TOOL_CALL`). - Deliberately not added here — the WO fixes the v1 hook catalog. -- **Job progress**: `plugin.JobHandlerFunc`'s `progress(current, total, - message)` callback needs a `jobs.progress` host function. -- **Bridge calls**: `bridge.register_service`/`get_service` cover - registration/lookup only; typed cross-plugin *invocation* (today: shared - in-process Go values) needs a runtime design. -- **Directory extension callbacks**: `panel_section_count` / - `pin_decorator_count` declare the guest callbacks; invoking them needs a - hook. - **Slot/media/embed resolvers in render**: container-slot rendering and media/embed resolution during a guest render need host functions (or host- - side pre-rendering into `RenderContext`). + side pre-rendering into `RenderContext`). The powered-block path sidesteps + this — host-side rendering has the real resolvers.