Cap is the sole captcha provider in the CMS; the Cloudflare Turnstile
TurnstileSiteKey field, settings read, and turnstileScript CDN injector are
retired from the shared bn head template.
PluginManifest gains `codeless` (additive, buf-breaking clean): an artifact
with NO plugin.wasm that the host runs entirely. `ninja plugin build`
classifies by repo shape — no Go source → codeless (manifest synthesized from
plugin.mod + optional manifest.yaml: theme presets, fonts, settings schema,
master pages, CSS, icon packs, deps); Go source → wasm as always. New
`--codeless` flag asserts the expectation.
Both artifact kinds now pack the declarative dirs: blocks/ (the cms
blocks.yaml manifest-FS layout — definition-backed blocks), templates/, and
seed/ (seed.json: settings/media/pages/menu items, applied host-side via the
WO-WZ-019 provisioner). The packer structurally validates blocks.yaml and
seed.json (schema JSON validity, file presence, traversal safety); full
semantic validation stays host-side at install.
`ninja plugin verify`: plugin.wasm is required exactly when NOT codeless; a
codeless manifest declaring any computing hook (http/jobs/load/unload/media/
RAG/tags/filters/services/guest blocks/data_dir) is rejected —
CodelessHookViolation mirrors the cms reader check (lockstep duplication).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
The cms host parity gate requires one golden per registered handler; this
completes the set so TestGoldenParity's count check holds.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Dynamic families added to the ABI + guest SDK:
- provisioner.* (14 methods, 1:1 plugin.Provisioner): wasm provisioning was
silently dead (RegisterWithProvisioner got a noopProvisioner and the cms
loader ignored has_provisioner). Now a LOAD-TIME capability via the new
CoreServices.Provisioner field; EnsureEmbed rejects RenderFunc-only embeds.
- content.* writes (content.Author + CoreServices.ContentAuthor):
create_page, set_page_blocks, publish_page, set_page_seo, upsert_post.
- settings.update_plugin_settings (settings.Updater grows the method).
- bridge.invoke + plugin.BridgeInvokable: opaque-payload cross-plugin calls
(typed GetService still returns nil across the sandbox by design).
- jobs.progress: HOOK_JOB handlers' progress() now crosses (was discarded).
New host→guest hooks: HOOK_AI_TOOL_CALL (executes recorded ai.ToolDefinition
handlers — registers tools in Register so every pooled instance has them),
HOOK_BRIDGE_CALL, HOOK_DIRECTORY_PANEL_SECTION, HOOK_DIRECTORY_PIN_DECORATOR.
The ai/bridge stubs now record handlers/values locally in addition to
forwarding names.
buf breaking clean (additive within ABI major 1); golden round-trips added
for the new families (cms host replays the same goldens).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
Adds ListPosts(ctx, ListPostsParams) to the content.Content capability so
wasm plugins can list posts with bodies — the per-plugin Postgres role
denies direct public.blog_posts reads (42501) and GetPost only fetches a
single post's metadata. Extends PostInfo with Body, AuthorName, AuthorSlug
and PublishedAt (additive; existing get_post golden unchanged). Wires all
layers: interface, ABI PostInfo/ContentListPosts{Request,Response} messages
(buf breaking clean), guest stub, and a deterministic content_list_posts
golden replayed by the cms host parity test.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two shared wasm-boundary fixes surfaced by the messenger port (WZ-013):
1. uuid[] DbValue variant. bnwasm had no uuid-array bind/scan, so every
ANY($1::uuid[]) query broke in the guest ("unsupported argument type
[]uuid.UUID") and messenger worked around it with a ::text[]::uuid[] cast.
Adds a dedicated DbValue.uuid_array_value (abiv1.UuidArray) — distinct from
text[] so the host binds a native uuid[] param (queries keep ::uuid[]) and
scans a uuid[] column straight into []uuid.UUID. Guest toDbValue marshals
[]uuid.UUID; naturalValue/assign parse the canonical strings back into
[]uuid.UUID (nil→NULL, empty stays empty). Pinned by the uuid_array entry in
the shared DbValueFixtures contract (round-trip + driver-value tests green).
2. Trusted identity headers (auth/trustedheaders.go). Context does not cross
the ABI, so guests cannot see the host's verified principal. The SECURE
contract: the host runs its RBAC guard against the signature-verified JWT,
strips any client-supplied copy of the X-Bn-Verified-* headers, and sets
them itself from auth.Get{Public,}UserFromContext; the guest reconstructs
context via auth.TrustedHeaderMiddleware and trusts ONLY those headers.
Guests MUST NOT decode a client cookie/Bearer token for identity — that is a
privilege-escalation bug (a verified public user forging an admin JWT the
guest would honour on a RolePublic method). Documented in docs/wasm-abi.md,
replacing the ambiguous "auth context reaches the guest via HttpRequest
headers" line that invited the insecure decode.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Document HostServices() as the per-instance block-pool escape hatch, and the
instance-pool sizing decision from the symposium port: pool=4 / 512 MiB per
instance validated against the 45 MiB symposium module (p50 5.5ms / p95 9.4ms
per 3-block page; ~628 MiB RSS at pool=4, fits the 4 GiB container default).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Block/template render funcs receive only ctx+content over the ABI, no
services. Load runs on a single pooled instance, so a DB-backed block on
any other pooled instance had a nil pool. HostServices() exposes the
CoreServices bound at _initialize on every instance (live db.* Pool +
capability stubs) so render funcs resolve their pool per-instance. Zero
value in native builds (Serve never called); callers nil-check Pool.
Enables the symposium wasm port (WO-WZ-012).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
`ninja plugin publish --bnp <file>` uploads a .bnp built by `ninja plugin
build` instead of a source archive. The orchestrator verifies the .bnp
layout/ABI/name/version server-side and records its abi_version. Without
--bnp, publish keeps shipping a source archive for backwards compatibility
until the CMS install path is wasm-native.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
`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>
- driver_test.go: use slices.Contains instead of a hand-rolled loop
- sqlcgen_test.go: interface{} -> any in the generated-style DBTX shim
- caps_roundtrip_test.go: new(idParent.String()) instead of proto.String
for the pointer literal
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Close three WO-WZ-004 review gaps in the guest db driver:
- toDbValue: nil []string now marshals to DbValue_Null (matching []byte /
json.RawMessage); empty-but-non-nil stays a non-NULL empty text[].
- DbValueFixtures: add edge entries (zero time.Time, negative + very-large
numeric strings, empty text[], and text[] elements forcing encodePgTextArray
quoting/escaping). Covered automatically by the table-driven round-trip and
driver-value tests; new dbvalue_test.go covers the toDbValue nil convention.
- Document that TextArray cannot represent a NULL array element (repeated
string has no per-element NULL) in the fixtures file and docs/wasm-abi.md,
a contract limit the WO-WZ-007 host executor must also honor.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Bind the db.* driver over the same transport as the capability stubs so
deps.Pool keeps working for plugin sqlc code:
- dispatch.go: g.services.Pool = bnwasm.NewPool(capTransport) (nil on
native/DESCRIBE → fails cleanly, matching the caps stubs).
- hostcalls.go (wasip1): also bind the "bnwasm" database/sql driver's
process-global transport to CallHost.
- docs/wasm-abi.md: document the implemented guest driver + pgx-primary
rationale under the Pool disposition.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Guest-side database access over the db.* host calls (abiv1 db.proto),
two surfaces sharing one injected transport:
- database/sql driver registered as "bnwasm" (driver.go) — QueryContext/
ExecContext/BeginTx over db.query/db.exec/db.tx_*; named args rejected.
- plugin.Pool (pool.go) handing out a pgx.Tx-shaped value (tx.go), so the
pgx-flavored sqlc DBTX every current plugin generates against
(sql_package: pgx/v5) is satisfied with no source edits. This is the
primary path: their DBTX needs pgconn.CommandTag/pgx.Rows/pgx.Row, which
database/sql cannot produce.
DbValue↔Go mapping (dbvalue.go) covers all 11 oneof arms both directions;
DbError surfaces as *pgconn.PgError (SQLSTATE preserved for errors.As);
nested tx/savepoints rejected with a clear error (no fleet plugin uses
them). The scan contract is pinned in the exported DbValueFixtures table
(dbvalue_fixtures.go) that the WO-WZ-007 host executor mirrors.
Tests: driver_test.go (fake host — every DbValue variant round-trips with
correct scan types, exec rows-affected, ordered host-call assertions for
tx commit/rollback sequences, post-rollback autocommit carries no handle)
and sqlcgen_test.go (vendored sqlc-style Queries + WithTx run against the
fake host). Native + wasip1 builds green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Every CoreServices interface (core/plugin/deps.go) now has a guest-side stub
that marshals to the WO-WZ-001 capability messages and dispatches through the
generic host_call transport, so plugin service code compiles and runs
unchanged against content.Content, settings.Settings, plugin.PluginBridge, etc.
- core/plugin/wasmguest/caps/: one file per family (17 families, 38 methods),
a var _ <iface> = (*stub)(nil) compile proof each, and NewCoreServices(call)
assembling them. The transport is injected (CallFunc) so marshaling is
natively testable; the wasm shim binds it to CallHost, DESCRIBE probes pass
nil (capability calls fail cleanly instead of nil-panicking).
- Error mapping wraps AbiError with <family>.<method> context and maps
DEADLINE_EXCEEDED onto context.DeadlineExceeded.
- RAGService.RegisterContentFetcher stays guest-side (RAGStub) for
HOOK_RAG_FETCH dispatch; Query/OnContentChanged marshal out. dispatch.go and
describe.go now source fetchers from the caps RAG stub.
- caps_roundtrip_test.go: fake transport + 76 deterministic golden payloads
(family_method_{req,resp}.pb) covering 100% of families, plus error-mapping,
deadline, nil-transport, and guest-side-fetcher tests. WO-WZ-006 replays the
same goldens to prevent host/guest drift.
- Acceptance: testdata/fixture Load hook calls deps.Content/Settings/Bridge
unchanged (compiles for wasip1); caps_wasmhost_test.go drives it end-to-end
through a real wazero module + fake host_call table.
- Disposition table in docs/wasm-abi.md: every member stub | host-side
(Pool, Interceptors, AppURL/MediaPath, CoreServiceBindings host-side).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A successful hook whose response message has no set fields (LoadResponse,
UnloadResponse) proto-marshals to zero bytes. bn_invoke's `len==0 → return 0`
shortcut collided with the "callee could not produce an envelope" sentinel, so
every successful empty-response hook — notably HOOK_LOAD — looked like an
INTERNAL failure and got the instance discarded. Frame the empty case as
(ptr, 0) with a real 1-byte-backed pointer instead; the host reads zero bytes
into a valid empty InvokeResponse.
Surfaced by the WO-WZ-003 end-to-end capability test (first exercise of LOAD).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Pre-existing strict-lint failures in the publish warning helpers were
the only thing keeping check-safety's Go lint lane red for the module;
discard the writer errors explicitly (warnings are best-effort output).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
testdata/fixture is the acceptance fixture: one block, one template, one
admin page, plus the two-line reactor boilerplate main. wasmhost_test.go
compiles it with GOOS=wasip1 GOARCH=wasm -buildmode=c-shared (15.7 MiB,
the WO-WZ-012 memory-budget baseline) and drives it through wazero
exactly per wasm-abi.md: _initialize as the start function, request
bytes through bn_alloc, DESCRIBE returning a decodable manifest,
same-instance block+template renders, a panicking BlockFunc surfacing as
ABI_ERROR_CODE_INTERNAL with the instance still callable, and a
non-bn_alloc request pointer rejected as DECODE.
wazero v1.12.0 joins go.mod as a test-only dependency (approved).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The wasip1 half of the ABI: bn_alloc/bn_invoke/bn_free exports with a
live-pin map so the GC never frees host-visible buffers, the generic
`blockninja.host_call` import (single import decided over per-family
symbols; recorded in wasm-abi.md), and a dispatch table adapting an
unmodified plugin.PluginRegistration to all nine v1 hooks. Panics inside
plugin hooks come back as ABI_ERROR_CODE_INTERNAL — the instance stays
callable; traps stay reserved for runtime corruption.
DESCRIBE builds the PluginManifest from the registration's static funcs
plus a capture-only Register pass (block metas via the same
PluginBlockRegistry prefixing the .so loader applies, template/system/
page-template/email-wrapper keys), probes JobHandlers/ServiceHandlers/
Load with capture-only services for job types, RBAC roles, core-service
bindings, and RAG fetcher types. RenderContext values are rehydrated
through the exact core/blocks context keys, so existing block code
reading from ctx works unchanged.
Plugins build in REACTOR mode (go build -buildmode=c-shared): init()
calls wasmguest.Serve (non-blocking), main is never called, and the host
runs _initialize before any bn_invoke. Command mode deadlocks or exits
(verified against wazero v1.12.0) — documented prominently in
wasm-abi.md, which also now reconciles the import module namespace to
`blockninja` and requires bn_alloc'd buffers on both directions.
Dispatch/describe/context logic is buildable on every GOOS; only
exports.go and hostcalls.go carry the wasip1 tag. dispatch_test.go
covers describe, hook routing, envelope mismatch, decode failures,
template-override resolution, panic recovery, and lifecycle hooks
natively.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
BlockNote >=0.15 (the editor ships 0.47) stores each table cell as a
{type:"tableCell", content:[...]} object. The renderer passed each cell
straight to inlineContentFromRaw, which only understands strings and bare
inline arrays, so object-form cells rendered an empty grid (correct rows
and columns, no content). Unwrap content[] from tableCell objects while
keeping the legacy bare-array/string cell formats working.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
SDK_DOWNSTREAM_DIRS pointed at the pre-consolidation layout
(~/src/blockninja/backend, ~/src/orchestrator/backend, ~/src/blockninja-themes,
~/src/{assumechaos,bidbuddy,...}) — none of which exist after all repos moved
under ~/src/blockninja/. Repoint at cms/backend + orchestrator/backend and
wildcard themes/ sites/ plugins/ so update-sdk/distribute-sdk reach them.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add the SDK surface for plugins to deposit images into the CMS media library.
MediaDeposit / MediaResult types and a Media interface on CoreServices for
runtime deposits; EnsureMedia on the Provisioner interface for idempotent
seed-time deposits under a plugin-chosen, template-referable UUID. The CMS
implements both against one internal depositor; seeded templates reference
the chosen UUID directly via {% img "<id>" %}.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds TestMutateTags_AddRmSetClear (covers add/rm/set/clear operations
including dedupe and normalisation) and TestMutateTags_RejectsInvalidNoWrite
(ensures validation failures don't mutate plugin.mod).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds ninja plugin tags (show), add, rm, set, and clear subcommands that
read and mutate the local plugin.mod tag list via NormalizeTags. The bare
tags command stubs ListTags with a TODO(tags): marker for Task 12.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add promptTagsWithDefault helper (mirrors promptCategoriesWithDefault) and
wire it into the init flow so upsertPluginMod receives real user-entered tags
instead of nil. ListTags RPC stub left with TODO(tags) marker for Task 12.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Extend upsertPluginMod signature to accept tags parameter (positional arg 7,
between categories and private). Update the single call site to pass nil.
Add tags serialization in writeMod, mirroring the categories pattern.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
TDD approach: added failing tests in mod_test.go that check parsing and
null-coalescing of tags, then added the []string Tags field to ModPlugin
struct with TOML tag "tags,omitempty".
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Lets plugins declare icon-pack dependencies (e.g. "tabler", "phosphor")
in plugin.mod and PluginRegistration. The CMS loader auto-installs
declared packs from the bundled registry before the plugin loads.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
CMS Go module renamed from git.dev.alexdunmow.com/block/ninja to
git.dev.alexdunmow.com/block/cms (along with the git remote rename
on gitea). All import paths, string-literal rules, test fixtures,
and doc references updated; (historical 'ninja-orchestrator' refs
preserved). go mod tidy regenerated checksums.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Repos consolidated under ~/src/blockninja/ parent (collection, not
monorepo). This repo moved from ~/src/core to ~/src/blockninja/core.
Updates historical plan/audit docs that referenced the old paths.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>