23 Commits

Author SHA1 Message Date
Alex Dunmow
1a75edeae9 chore: rename dev host localdev.blockninjacms.com -> blockninja.dev 2026-07-05 20:50:19 +08:00
Alex Dunmow
c7cbf69f63 refactor(abi): shed ABI contract + ninja CLI (WO-WZ-023)
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>
2026-07-04 22:37:24 +08:00
Alex Dunmow
3ce6f9f4a0 feat(bnp): fold manifest.yaml into wasm builds for mixed-form artifacts (WO-WZ-021)
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>
2026-07-04 19:50:34 +08:00
Alex Dunmow
87bbf9fe46 feat(bnp): codeless template overrides + email wrappers (WO-WZ-021)
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>
2026-07-04 13:52:32 +08:00
Alex Dunmow
52d7413aa0 feat(bnp): codeless system/page templates — layouts without code (WO-WZ-020)
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>
2026-07-04 11:50:38 +08:00
Alex Dunmow
ca9332180d docs: codeless-bnp.md — the declarative/logic split and artifact contract (WO-WZ-020)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 11:38:44 +08:00
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
Alex Dunmow
04991295cf feat(abi,blocks): ninjatpl rendering as a host capability (powered blocks + tag/filter callbacks)
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>
2026-07-04 02:36:39 +08:00
Alex Dunmow
314a25353d feat(abi,auth): first-class uuid[] DbValue + trusted identity headers (WZ-016)
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>
2026-07-04 00:43:13 +08:00
Alex Dunmow
05ded99d32 docs(wasm-abi): per-instance HostServices + symposium pool/memory gate (WO-WZ-012)
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>
2026-07-03 21:57:29 +08:00
Alex Dunmow
ad6d87bf06 feat(ninja): plugin build/verify → .bnp packer + ABI riders (WO-WZ-009)
`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>
2026-07-03 18:34:17 +08:00
Alex Dunmow
40ba4ee9de bnwasm: nil []string→NULL, fatten DbValue fixtures, doc text[] NULL-element limit
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>
2026-07-03 14:49:49 +08:00
Alex Dunmow
081bacf2ab feat(wasmguest): wire bnwasm Pool into guest CoreServices (WO-WZ-004)
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>
2026-07-03 14:41:56 +08:00
Alex Dunmow
bec3a43f55 feat(wasmguest): guest capability stubs over host calls (WO-WZ-003)
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>
2026-07-03 14:23:58 +08:00
Alex Dunmow
c35199f0bc feat(wasmguest): guest runtime shim for the wasm plugin ABI (WO-WZ-002)
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>
2026-07-03 13:53:24 +08:00
Alex Dunmow
704b046727 feat(abi): wasm plugin ABI protobuf schema v1 (WO-WZ-001)
Defines the host<->guest wire contract for the .so -> wazero migration as a
repo-local buf module (abi/proto/v1, generated Go in abi/v1):

- manifest.proto: PluginManifest mirroring every static field of
  plugin.PluginRegistration (abi_version, deps, block/template registrations,
  admin pages, settings schema, theme presets, fonts, master pages, AI
  actions, RBAC method roles, CSS manifest, icon packs, directory
  extensions, job types, RAG fetcher types, settings panel, hook flags,
  core service bindings)
- invoke.proto: bn_invoke envelope, Hook catalog (RENDER_BLOCK,
  RENDER_TEMPLATE, HANDLE_HTTP, JOB, LOAD, UNLOAD, RAG_FETCH, MEDIA_HOOK,
  DESCRIBE), AbiError, and job/lifecycle/rag/media/describe payloads
- render.proto: RenderBlock/RenderTemplate payloads + RenderContext
  enumerating every ctx value from blocks/context.go (incl. BlockContext 1:1)
- http.proto: buffered HttpRequest/HttpResponse for HANDLE_HTTP
- db.proto: guest sql-driver messages (DbValue oneof over pgx-mappable
  types, query/exec/tx with host-side tx handles, SQLSTATE-carrying DbError)
- capability.proto: HostCall envelope + one request/response pair per
  CoreServices interface method (content, settings+update, gating, crypto,
  menus, datasources, users, subscriptions, media.deposit, email.send,
  ai.text_call, ai.tools.register, bridge, jobs.submit, embeddings, rag,
  reviews, badges.refresh)

The abi/ buf module is deliberately separate from the repo-root config:
proto/ is the shared block/proto submodule (service API contracts); the ABI
is SDK-internal and versions in lockstep with the guest shim. New `make abi`
target runs buf lint + generate. docs/wasm-abi.md documents the ptr+len
calling convention (bn_alloc, bn_invoke, packed u64), hook catalog, error
semantics, abi_version evolution rules, and the full registration/
CoreServices coverage tables.

Verified: `cd abi && buf lint` exit 0; `go build ./...` exit 0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 12:47:00 +08:00
Alex Dunmow
f4e579ad7a refactor: simplify pongo template context and engine, add private plugins spec
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-08 20:49:18 +08:00
Alex Dunmow
c3c7b2d441 docs: follow CMS module rename
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>
2026-06-06 13:55:49 +08:00
Alex Dunmow
48c54814ec docs: update paths after 2026-06-06 repo consolidation
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>
2026-06-06 13:02:51 +08:00
Alex Dunmow
7615bd92ca feat(cli): multi-account login, private-plugin SDK, publish dirty handling
- ninja login forces account selection (interactive when >1); creds now
  carry ActiveAccountID/Slug. New `ninja account` group.
- ninja plugin list / delete / delete-version split public vs active-account
  @private sections; `publish --private` is sticky in plugin.mod.
- GetPluginRequest gains active_account_id so @private resolution works
  alongside the public (scope, name) path.
- publish auto-commits a dirty plugin.mod (path-scoped, leaves other staged
  paths alone) so the bump→publish loop never trips the dirty check.
  --allow-dirty is replaced with --strict (default now ships dirty trees
  via stash-create).
- bump auto-commits its plugin.mod write with `bump to X.Y.Z`; --no-commit
  opts out.
- Design doc updated to match the new defaults.
2026-06-04 08:49:23 +08:00
Alex Dunmow
7af42c1c83 docs(uat): tick all 86 UAT boxes after end-to-end observation 2026-06-03 07:26:13 +08:00
Alex Dunmow
e9bef5b065 docs: plan, UAT, and execution prompt for plugin tarball publish + categories 2026-06-03 01:18:13 +08:00
Alex Dunmow
1d9ca44f55 docs(spec): plugin publish (tarball) + categories design
Design for two coupled changes: drop git as the publish transport in
favour of tar.zst uploads, and add a first-class plugin kind plus a
configurable, validated category list.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-03 00:58:21 +08:00