core/docs/abi-capability-surface.md
Alex Dunmow 925b6051a3 docs(abi): capability-surface matrix + wasm-abi.md updated for WO-WZ-019
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 <noreply@anthropic.com>
2026-07-04 10:28:44 +08:00

100 lines
8.6 KiB
Markdown

# 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:<id>`, …) + `.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:<uuid>` 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.