Purpose sharpened per Captain's direction: core exists solely for code
shared between two or more first-party entities — cms, orchestrator, the
ninja CLI, or future entities. Adds the admission test (single consumer →
that repo, plugin-needed → wasm ABI), the shrink-core direction, and the
templates/bn synced-copy rule (authored in cms, make sync-templates,
check-safety 31, validation.go intentionally divergent).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
cms is now the authoring source for the bn chrome (head/toolbar/engagement);
this repo carries a mechanically-synced copy solely for guest-side plugin
templates (cms `make sync-templates`, drift gated by check-safety check 31).
head.templ picks up everything it had drifted behind on (RFG-parity favicon
head, analytics/web-vitals beacons, custom-scripts placement, bnApplyTheme)
and is now SDK-clean: asset versioning goes through the new
bn.VersionedAssetURL hook (identity default; the CMS host wires it to its
internal assets registry). HeadData is shape-compatible with existing plugin
usage; BrandingData/SiteSettingsData internals changed — plugins see that at
compile time on their next core bump.
Spec: cms docs/superpowers/specs/2026-07-07-bn-chrome-single-source-design.md
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sync the vendored templates/bn/toolbar.templ with the cms copy (entrance
animation, position picker chips, HidePreviewToggle — and the new per-tab
theme tester button). Port themeInitScript to the override-aware
window.bnApplyTheme so jutsu-rendered pages honour the admin's transient
bn-theme-override (sessionStorage) ahead of the visitor bn-theme preference.
Regenerated *_templ.go with templ v0.3.1020 (button/engagement diffs are
generator-version churn only).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Pick up ninjatpl perf release (pooled tag-body buffers, lexer escape
fast-path). No API change; templates/pongo builds and behaves identically.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The legacy templates/pongo host runtime now uses the in-house ninjatpl
fork. Mechanical import/ident swap — these files only use NewSet /
TemplateLoader / Context / Must / Template, all identical in ninjatpl;
no custom filters/tags or *Error signatures involved.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The wasm-plugin ABI contract now lives in the cms (block/cms/abi/v1 + docs) and
the ninja CLI moved to its own repo (block/cli). Core keeps abi/ as the guest
SDK until WO-WZ-027. Removes cmd/ninja, the dead orchestrator registry client
(internal/api/orchestrator), the moved docs, and the now-unused ninja/orchclient
deps from go.mod/go.sum.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A wasm plugin.build now folds an optional root manifest.yaml into the
DESCRIBE-derived manifest.pb exactly as BuildCodeless does, so a reduced
"mixed" plugin can keep a minimal guest for genuine logic while shipping the
full declarative surface set host-rendered: theme_presets, bundled_fonts,
master_pages, system/page templates, template_overrides, email_wrappers, css,
required_icon_packs (and the referenced root JSON is embedded into manifest.pb).
applyManifestYAML now only overwrites a scalar/bytes key when manifest.yaml
actually declares it, so folding onto a guest-populated manifest supplements
and overrides but never WIPES a guest DESCRIBE field. A repo with no
manifest.yaml is a no-op — existing pure-wasm plugins build byte-for-byte as
before. Codeless builds are unaffected (empty manifest → identical result).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Core-dissolution direction (Captain's ruling, WO-WZ-026/027): host-side
machinery must not grow in core — the engine additions now live in cms
as backend/plugin/pongoengine (absorbed copy). Tag v0.19.1 still points
at the reverted commit; nothing consumes it. The v0.19.0 codeless
BUILDER keys stay: the ninja CLI lives here until WO-WZ-023 moves it
into cms.
This reverts commit 630dfc7dd1cd35b46482d81904f77dbbcb01c885.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
PageTemplate/BlockTemplate/EmailWrapper return errors instead of
panicking so hosts can compile artifact-supplied templates at load time
(codeless themes) without a malformed artifact taking down the process;
Must* variants delegate. The 'lines' filter splits a string into
trimmed non-empty lines — pongo2 string literals cannot express \n, so
multi-line textarea fields (per-line <div> addresses) were previously
inexpressible in templates.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
manifest.yaml gains template_overrides [{template, block}] and
email_wrappers [system keys]; sources by convention at
templates/overrides/<template>/<block>.ninjatpl and
templates/email/<system>.ninjatpl (validated at build). Populates the
existing manifest fields BlockTemplateOverrides / EmailWrapperSystemKeys —
no proto change. Unblocks full theme-catalog codeless conversion: every
theme in the fleet registers overrides + an email wrapper.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
VerifyToken now consumes a redemption token on its first successful verify:
a replayed token within its 5-min TTL is rejected. Mirrors the existing
single-use challenge-nonce store with a per-Server used-token store keyed on
the token's random id, same GC/expiry approach (entries live only for the
remaining TTL). The token is burned only on a successful verify (valid HMAC +
unexpired + unused); forged/expired tokens never touch the store.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
manifest.yaml gains system_templates + page_templates declarations; each page
template's .ninjatpl source lives at templates/<system>/<key>.ninjatpl and is
validated at pack time. Verify no longer rejects declared system/page
templates on codeless manifests (guest template_keys stay rejected) — the cms
host registers them source-tracked and renders the files through the same
host pongo pipeline powered blocks use. Blog/system/normal page layouts are
now fully expressible with zero code.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>