feat(plugins/runtime): wazero-backed WASM runtime foundation (#6)#350
Merged
Conversation
Adds packages/go/plugins/runtime/ as the foundation layer for the GoNext plugin host. Wraps tetratelabs/wazero@v1.11.0 with a small, opinionated API: - Runtime — owns the wazero runtime, registers the built-in "env" host module, hands out Module instances. - Module — instantiated WASM module; goroutine-safe Call (per-module mutex serializes wazero's non-thread-safe Function.Call). - host.go — registers gn_log, gn_panic, gn_time_ms. gn_panic stashes its decoded reason then closes the module so the trap surfaces as a *TrapError with the guest's message. - WithHostModule extension seam so the capability ABI (#107) can register additional host modules without touching this package. - 256-page (16 MiB) per-module memory cap as the v1 placeholder until the real resource-limits story (#15) lands. Tests cover: happy-path Call (add(7,35)=42), guest gn_panic surfaces as TrapError with the panic reason, malformed bytes -> CompileError (no panic leak), memory cap rejects oversized modules cleanly, concurrent calls into the same module from 16 goroutines x 64 calls under -race, Close-during-Call drains in-flight invocations. Pool (#9), real limits (#15), and the capability ABI (#107) build on this — none are in scope for this PR. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Signed-off-by: Mohamed Tayeb Mokni <tayeb.mokni@gmail.com>
This was referenced May 17, 2026
tayebmokni
added a commit
that referenced
this pull request
May 17, 2026
…356) ## Summary - New `packages/go/plugins/runtime/limits` package: `Limits{MemoryPages, CPUTimeoutSoft, CPUTimeoutHard, MaxInstancesPerPlugin}`, `Default()`, `Validate()`, `Enforcer` (CPU deadline wrapping + per-plugin instance Acquire/Release). - Runtime gains `WithLimits(l Limits)` option and `Enforcer()` accessor; every `Module.Call` now applies the enforcer's soft/hard deadline pair to the call context. Wazero's `WithCloseOnContextDone(true)` (already set in #350) turns cancellation into a guest trap. - `tightloop.wat` fixture + hand-encoded binary so CPU-deadline tests have a guest with no natural exit path. ## Test plan - [x] `go vet ./plugins/runtime/limits/...` — clean - [x] `go test -race -count=1 ./plugins/runtime/limits/...` — green (validation, soft+hard timer fire ordering, concurrent Acquire under -race, idempotent release, per-name counters) - [x] `go test -race -count=1 ./plugins/runtime/...` — green (soft timeout traps tight loop ~100ms, hard timeout-only path also traps, fast call with zero limits stays fast, memory cap below `bigmem`'s 1024 pages rejects load, default envelope kills a tight loop within 5s) - [x] Invalid Limits (hard < soft) rejected at `New()` time 🤖 Generated with [Claude Code](https://claude.com/claude-code) Signed-off-by: Mohamed Tayeb Mokni <tayeb.mokni@gmail.com> Co-authored-by: Mohamed Tayeb Mokni <tayeb.mokni@gmail.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
tayebmokni
added a commit
that referenced
this pull request
May 17, 2026
… (#357) ## Summary - Adds `packages/go/plugins/runtime/pool/` — a fixed-bounded pool of pre-warmed wazero modules so per-request plugin calls (filter hooks, REST handlers) skip the 1–5 ms instantiation tax. - `Pool.Checkout(ctx) -> Lease` blocks up to the caller's deadline; `Lease.Return()` recycles or restocks; `Lease.MarkUnusable()` retires a trapped instance. - Reaper goroutine evicts idle instances past `MaxIdleTime` and refills back to `MinInstances`. `MaxUsesPerInstance` rotates aging instances to prevent linear-memory bloat. - Prometheus surface: `checkout_total`, `checkout_errors_total`, `checkout_wait_seconds`, `recycle_total{reason=trap|max_uses|idle|close}`, `pool_size`, `in_use` — all under `gonext_plugin_pool_*`. - Builds on PR #350's wazero `Runtime` / `Module` shapes without modifying them. ## Test plan - [x] `go vet ./plugins/runtime/pool/...` clean - [x] `go test -race -count=1 ./plugins/runtime/pool/...` green (full suite ~6 s) - [x] Checkout/Return cycle - [x] Concurrent checkouts saturate at `MaxInstances`; (Max+1)th blocks until Return - [x] `ctx.Deadline` timeout while blocked — wrapped `ErrCheckoutTimeout` + `context.DeadlineExceeded`, no leak - [x] `MaxUsesPerInstance` rotates the underlying module pointer after N uses - [x] `MaxIdleTime` reaper evicts idle and refills to `MinInstances` - [x] Trap-flagged lease forces recycle on Return, never re-enters rotation - [x] `Pool.Close` drains outstanding leases; honors ctx deadline; second Close is no-op - [x] Race test: 100 goroutines × 1000 checkouts under `-race` — zero races, zero leaks Closes #9. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Signed-off-by: Mohamed Tayeb Mokni <tayeb.mokni@gmail.com> Co-authored-by: Mohamed Tayeb Mokni <tayeb.mokni@gmail.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
tayebmokni
added a commit
that referenced
this pull request
May 17, 2026
## Summary Bridges the host-side action+filter bus (`packages/go/hooks`, PR #318) to plugin WebAssembly modules loaded by the wazero runtime (`packages/go/plugins/runtime`, PR #350). At activation time the lifecycle Manager walks the manifest's `hooks` declarations; for each declared hook the bridge installs a callback on the host bus that proxies through to the plugin's `gn_handle_hook` export. Closes #95. ## The ABI Every plugin exports: ``` (func (export "gn_handle_hook") (param $name_ptr i32) (param $name_len i32) (param $payload_ptr i32) (param $payload_len i32) (result i64)) (func (export "gn_alloc") (param i32) (result i32)) ;; bump or arena allocator (func (export "gn_free") (param i32 i32)) ;; counterpart ``` The packed `i64` return is `ptr<<32 | len`. Special cases: - `(0, 0)` — success, no body (action return) - `(0, negative)` — typed `ResultStatus`: `error` / `out_of_memory` / `bad_payload` / `unknown_hook` - `(ptr, len>0)` — success with body at `[ptr, ptr+len)` in the guest's exported memory; the host copies and then calls `gn_free` Payloads are JSON envelopes (`{"kind":"action","args":[…]}` or `{"kind":"filter","value":…,"args":[…]}`); filter results are `{"value":…}`. ## What's here - `abi.go` — contract documentation, `ResultStatus` catalog, `HookError` type with `errors.Is` matrix across sentinels - `marshal.go` — JSON envelopes for action and filter payloads - `dispatcher.go` — `Dispatcher` drives the ABI on a wazero `Module` (`InvokeAction` / `InvokeFilter`); caches export lookups; surfaces traps via `ResultStatusTrap` - `registry.go` — `Bridge` walks `manifest.Hooks`, installs proxy callbacks on the host bus; `Unregister` detaches them all - `wat/the_content.wat` — fixture plugin: `the_content` filter uppercases its input; `on_event` action returns OK-no-body; `trip_panic` action calls `gn_panic` for trap testing. Includes a bump allocator and a `force_oom()` test helper ## What's NOT here (separate issues) - Lifecycle wiring (Manager constructing the Bridge per activation) — the lifecycle Manager doesn't yet pull in this package; that wiring belongs to the activation flow refactor - Per-hook priority hints from the manifest — placeholder `PriorityFunc` hook in `NewBridge` until the manifest schema gains a `priority` field - ABI v2 (e.g. msgpack for hot-path hooks) — versioned via sibling package per the contract notes ## Test plan - [x] `go vet ./plugins/abi/hooks/...` clean - [x] `go test -race -count=1 ./plugins/abi/hooks/...` — 17 tests pass - [x] Filter happy path: `"hello"` → `"HELLO"` through `the_content` - [x] Action with no payload (empty action) — guest accepts empty args - [x] Action with args (string, int, map) marshals and dispatches - [x] Unknown hook → `ErrUnknownHook` (typed status) - [x] Plugin traps → `ErrTrapped`, underlying `*TrapError` unwrappable - [x] Guest OOM (bump pointer past memory end) → `ErrOutOfMemory` - [x] Host payload cap (>1 MiB) → `ErrPayloadTooLarge` before any guest call - [x] 100 concurrent invocations on the same Module: all correct, race detector clean - [x] `Bridge.Register` walks manifest, `bus.Do` and `bus.ApplyFilters` reach the plugin, `Bridge.Unregister` detaches cleanly - [x] Missing ABI exports surface as `ErrMissingExport` - [x] `HookError` `errors.Is` against `ErrOutOfMemory`, `ErrBadPayload`, `ErrUnknownHook`, `ErrGuestError`, `ErrTrapped` ## Notes for reviewers The fixture `.wasm` is committed as a generated Go slice (`fixturedata_test.go`) because the repo `.gitignore` excludes `*.wasm`. The `.wat` source plus a Python encoder (`wat/encode.py`) and Makefile let reviewers with `wat2wasm` regenerate in one command: `make -C wat`. The filename dodges two Go-tool quirks: - anything with a `testdata` prefix is ignored as test data - anything matching `*_wasm.go` or `*_wasm_test.go` is interpreted as `GOOS=wasm`-only `fixturedata_test.go` avoids both. There's a comment in `abi_test.go` explaining this for future archaeologists. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Signed-off-by: Mohamed Tayeb Mokni <tayeb.mokni@gmail.com> Co-authored-by: Mohamed Tayeb Mokni <tayeb.mokni@gmail.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
11 tasks
tayebmokni
added a commit
that referenced
this pull request
May 18, 2026
…loses #245 (#371) ## Summary Closes #245. Adds the `gonext plugin dev <project-dir>` subcommand for plugin authors: auto-detect the toolchain, build to WASM, upload to a running dev host, watch for changes, and hot-reload. Lives in `cli/gonext/cmd/plugin/`. Foundation merged so the plugin platform now has: wazero runtime (#350), manifest validator (#345), capability registry (#344), instance pool (#357), resource limits (#356), hook ABI (#364), lifecycle (#339). This PR is the productive author-side dev loop on top of all that. ### Flags ``` gonext plugin dev [flags] <project-dir> --host URL of the running gonext dev host (default http://localhost:8080) --watch Watch the project tree and hot-reload on change (default true) --build-only Build only; skip upload and watch --lang auto | go | tinygo | rust (default auto) ``` ### Architecture The orchestrator (`runDevLoop`) runs four phases — detect → build → upload → watch — and every I/O dependency funnels through an injectable seam: | Seam | Production | Test | |---|---|---| | `CommandRunner` | `execRunner` (exec.CommandContext) | `fakeRunner` recording argv + optional Hook to plant artifacts | | `Uploader` | `httpUploader` (multipart POST) | `stubUploader` counter / `httptest.Server` | | `Watcher` (factory) | `fsnotifyWatcher` (recursive Add) | `fakeWatcher` (buffered chans) | | `Now func()` | `time.Now` | frozen clock | That lets the watch+rebuild loop be unit-tested end-to-end without forking, real filesystem races, or network round-trips. 64 test cases across six files. ### Per-language build commands | Language | Detection | Command | Output | |---|---|---|---| | TinyGo | `go.mod` present | `tinygo build -o build/plugin.wasm -target=wasi .` | `build/plugin.wasm` | | Rust | `Cargo.toml` present | `cargo build --target wasm32-wasi --release` | Copied from `target/wasm32-wasi/release/*.wasm` to `build/plugin.wasm` | Both markers present produces an ambiguity error rather than a silent pick. The Rust path errors if cargo produces more than one `.wasm` (asks the author to set `[lib].name`). ### Watch loop - `fsnotify` recursively registers the project tree at startup, skipping `build/`, `target/`, `node_modules/`, `.git/`, `.idea/`, `.vscode/`, `vendor/`, `dist/`, `__pycache__/`. - Generic `debounce[T any]` with a 200ms trailing-edge window collapses bursty inotify events (single save → single rebuild). Tested with 20–30ms windows and synthetic event sources. - Editor tempfiles (`*~`, `.#*`) and our own `build/` writes are filtered so the loop doesn't feed itself. - First-pass build/upload errors propagate out (non-zero exit). Errors during watch are printed but the loop keeps running — the operator's next save is probably the fix. - SIGINT / SIGTERM → graceful exit 0. ### Capability diff After each successful build the manifest's `capabilities` list is compared against the previous snapshot: ``` [19:11:29] detected language: tinygo [19:11:29] build: tinygo [19:11:29] upload: http://localhost:8080 capabilities: = http.fetch = db.read [19:11:30] uploaded successfully [19:11:30] watching /path/to/plugin (Ctrl-C to stop) [19:11:42] change detected — rebuilding capabilities changed: + media.write - db.read ``` This is what gives the operator a clear before/after view between hot reloads — the capability set is what the host is being asked to grant. ## TODO — host-side endpoint follow-up This CLI uploads to `POST ${host}/_/plugins/dev/install` but the host-side endpoint is **not in this PR**. The contract this CLI assumes: - Path: `/_/plugins/dev/install` - Method: `POST` - Content-Type: `multipart/form-data` - Parts: - `manifest` — file part, `application/json`, the `manifest.json` bytes - `wasm` — file part, `application/wasm`, the compiled module - Success: any 2xx (body ignored) - Failure: any non-2xx; body's first 1KiB is included verbatim in the CLI's error message The natural host-side implementation calls the existing lifecycle Manager's `Install(ctx, bundle io.Reader)` — that function already validates the manifest and handles slug/version/abi extraction. The dev install endpoint would also need to bypass signature verification (dev-only) and idempotently re-activate on every upload. Followup issue suggested: \"feat(server): host-side dev plugin install endpoint\". ## Test plan - [x] `cd cli/gonext && go vet ./...` — clean - [x] `cd cli/gonext && go test -race -count=1 ./cmd/plugin/...` — 64 cases pass under `-race` - [x] `cd cli/gonext && go test -race -count=1 ./...` — entire CLI module green - [x] `cd cli/gonext && go build ./...` — builds - [x] Language detector: tinygo via `go.mod`, rust via `Cargo.toml`, ambiguous error when both present, missing-marker error, dir-not-file rejected - [x] Build orchestrator: TinyGo argv = `[build -o <abs>/build/plugin.wasm -target=wasi .]`, Rust artifact normalisation, multiple `.wasm` error path, runner failure propagates - [x] Multipart upload: httptest server roundtrip verifies path, Content-Type, both parts, byte fidelity; host trailing-slash + subpath join correctly; 4xx body preview surfaces; missing wasm errors; relative host rejected - [x] Debounce: 5-event burst collapses to 1 emission; two bursts 60ms apart fire twice; ctx-cancel closes out-channel; input-close closes out-channel - [x] Capability diff: first build prints `=` list, no-change emits nothing, add/remove deltas use `+`/`-` - [x] Dispatcher: `--build-only` skips upload + watch, watch+rebuild on event, watch keeps running through build error and recovers on next event, usage/help/missing-arg/extra-arg/not-a-directory exit codes - [ ] Manual smoke against a running dev host — gated on the host-side endpoint follow-up 🤖 Generated with [Claude Code](https://claude.com/claude-code) Signed-off-by: Mohamed Tayeb Mokni <tayeb.mokni@gmail.com> Co-authored-by: Mohamed Tayeb Mokni <tayeb.mokni@gmail.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
5 tasks
tayebmokni
added a commit
that referenced
this pull request
May 18, 2026
…#271 (#387) ## Summary - New `packages/go/plugins/debug` package: Source Map V3 parser, trap inspector with `<no source map>` graceful degrade, and a pub/sub `LogHub` + WebSocket handler at `/_/plugins/dev/logs/{plugin}`. - Runtime gains a `LogPublisher` seam (`WithLogPublisher`); `hostGnLog` forwards every `gn_log` call after the slog write, non-blocking. - `gonext plugin dev --logs` opens the WebSocket and prints color-coded log lines; client-side RFC 6455 handshake inline, no third-party websocket deps. ## Test plan - [x] `go vet ./plugins/debug/...` clean - [x] `go test -race -count=1 ./plugins/debug/...` — parser, inspector with/without map, hub fanout/filter/cancel/lag, WS round-trip, bad-path 400 - [x] `go test -race -count=1 ./plugins/runtime/...` — new `TestHost_GnLog_Publisher` plus full existing suite - [x] `go test -race -count=1 ./cli/gonext/cmd/plugin/...` — `--logs` URL builder, color mapping, CLI tailer round-trip via httptest WS upgrader, no-flag skip - [x] DCO sign-off included; no `--no-verify`, no amend Builds on #350 (Wazero runtime) and #371 (`gonext plugin dev`). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Signed-off-by: Mohamed Tayeb Mokni <tayeb.mokni@gmail.com> Co-authored-by: Mohamed Tayeb Mokni <tayeb.mokni@gmail.com>
4 tasks
tayebmokni
pushed a commit
that referenced
this pull request
May 18, 2026
Introduces packages/go/plugins/abi/jobs, the bridge between a plugin's manifest jobs[] declarations and the host TaskSpec + Asynq dispatch chassis. Mirrors the hooks bridge (abi/hooks, #350/#364) but for queued background jobs. The bridge: - Defines the gn_handle_job ABI (abi.go) with the same packed-i64 return shape as gn_handle_hook so plugin SDKs share most of their host-call plumbing. - Provides a Dispatcher (dispatcher.go) that drives the ABI on a plugin Module — serialised allocation, name+payload write, invocation, packed-result unpack, typed JobError on failure. - Provides a Bridge (bridge.go) that walks manifest.Jobs and registers one taskspec.TaskSpec per declared job, routed to the "plugin" Asynq queue (per #362), with the asynq Task ID threaded through the envelope as an idempotency key (per #363). - Gates registration on the jobs.enqueue capability via the plugin capabilities Checker (#356/#344). Manifests declaring jobs without the cap are rejected before any TaskSpec installs. Hot-reload contract: Unregister flips the closed flag so the installed TaskSpec.Handler short-circuits with a retryable error; the lifecycle Manager constructs a fresh Bridge against a fresh registry on plugin reload. Tests (22 cases, race-clean): - Pack/unpack round-trip and envelope marshal shapes. - Happy-path invocation, unknown job, trap, OOM, payload too large. - Bridge registers from manifest with correct queue/MaxRetry. - Capability denial rejects registration with no half-wired state. - Trap inside job returns retryable error (no asynq.SkipRetry). - Hot reload: gen1 unregister + gen2 register on a fresh registry. - Duplicate job name surfaces taskspec.ErrAlreadyRegistered. - 100 concurrent task pickups for the same plugin (race detector). Signed-off-by: Mohamed Tayeb Mokni <tayeb.mokni@gmail.com>
tayebmokni
added a commit
that referenced
this pull request
May 18, 2026
## Summary - Introduces `packages/go/plugins/abi/jobs` — the bridge between a plugin's manifest `jobs[]` declarations and the host TaskSpec + Asynq dispatch chassis (#355, #362). Mirrors the hooks bridge (#350, #364) for queued background jobs. - Defines the `gn_handle_job(name_ptr, name_len, payload_ptr, payload_len) -> i64` ABI with the same packed-i64 return shape as `gn_handle_hook`, so plugin SDKs share most of their host-call plumbing. - For each `manifest.Jobs` entry, registers a `taskspec.TaskSpec` routed to the `plugin` queue, with the asynq Task ID threaded through the envelope as an idempotency key (#363). Capability-gated on `jobs.enqueue` via the plugin capabilities Checker (#356, #344) — manifests declaring jobs without the cap are rejected before any TaskSpec installs. ## Test plan - [x] `go vet ./plugins/abi/jobs/...` clean. - [x] `go test -race -count=1 ./plugins/abi/jobs/...` — 22 cases pass: pack/unpack round-trip, envelope marshal shapes, happy-path/unknown/trap/OOM/oversize, manifest registration with correct queue + MaxRetry, capability denial leaves the registry untouched, trap returns a retryable error (no `asynq.SkipRetry`), hot-reload across two fresh registries, duplicate job surfaces `taskspec.ErrAlreadyRegistered`, and 100 concurrent task pickups for the same plugin. - [x] Full `./plugins/... ./jobs/...` test suite stays green. - [x] DCO sign-off present. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Signed-off-by: Mohamed Tayeb Mokni <tayeb.mokni@gmail.com> Co-authored-by: Mohamed Tayeb Mokni <tayeb.mokni@gmail.com>
Merged
5 tasks
tayebmokni
pushed a commit
that referenced
this pull request
May 19, 2026
…-to-end — closes #269 ## Summary - `examples/plugins/seo/` — a TinyGo-built plugin that ships `manifest.json` (apiVersion gonext.io/v1), four exported ABI symbols (`gn_alloc`, `gn_free`, `gn_handle_hook`, `gn_handle_job`), and a build script. Subscribes to `the_content` (filter, injects schema.org JSON-LD), `wp_head` (action, emits `<title>` + meta + OpenGraph + Twitter card), and `save_post` (action, computes SEO score). Owns one background job (`seo.recompute-scores`). - `domain.go` — pure Go helpers (`BuildTitle`, `BuildDescription`, `BuildHeadHTML`, `BuildJSONLD`, `ComputeSEOScore`) split out so the test suite exercises the same code the WASM blob links — no TinyGo required to run `go test`. - `dummy_host_test.go` — Go-side fake host bus that mirrors the wazero dispatcher path, proving the wire format the plugin speaks matches what `packages/go/plugins/abi/hooks/marshal.go` produces. Canonical copy at `packages/go/plugins/internal/_seo_dummy.go`. - `seo_test.go` — manifest validates against `packages/go/plugins/manifest/schema.json`; unit tests for all domain functions; HTML escaping; JSON-LD parses + carries Article required fields; pinned-score table; end-to-end filter run. - `docs/04-seo-plugin-tutorial.md` — a 30-minute "build this plugin from scratch" walkthrough. - `go.work` — adds `./examples/plugins/seo` so the example builds alongside the workspace; the example has its own `go.mod` so TinyGo doesn't drag in the host's Postgres/Redis/Asynq deps. The plugin uses every plumbed capability (`posts.read`, `posts.write`, `hooks.subscribe`, `jobs.enqueue`) so the install dialog has a real-world cap list to render. ## Test plan - [x] `cd /tmp/gn-i11 && go test -race -count=1 ./examples/plugins/seo/...` — 14/14 pass - [x] `cd packages/go && go test -race -count=1 ./plugins/lifecycle/... ./plugins/abi/hooks/...` — pass - [x] `cd packages/go && go test -race -count=1 ./plugins/manifest/...` — pass - [x] `cd examples/plugins/seo && bash build.sh` — exits with helpful "tinygo not on PATH" message when toolchain missing; documented in README - [ ] Manual: install TinyGo, run `./build.sh`, pack into `seo.gnplugin` via the documented zip recipe, install + activate via the lifecycle Manager (deferred — requires admin UI from #350) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Signed-off-by: Mohamed Tayeb Mokni <tayeb.mokni@gmail.com>
tayebmokni
added a commit
that referenced
this pull request
May 19, 2026
…-to-end — closes #269 (#403) ## Summary - `examples/plugins/seo/` — a TinyGo-built plugin that ships `manifest.json` (apiVersion `gonext.io/v1`), four exported ABI symbols (`gn_alloc`, `gn_free`, `gn_handle_hook`, `gn_handle_job`), and a build script. Subscribes to `the_content` (filter, injects schema.org JSON-LD), `wp_head` (action, emits `<title>` + `<meta>` + OpenGraph + Twitter card), and `save_post` (action, computes SEO score). Owns one background job (`seo.recompute-scores`). - `domain.go` — pure Go helpers (`BuildTitle`, `BuildDescription`, `BuildHeadHTML`, `BuildJSONLD`, `ComputeSEOScore`) split out so the test suite exercises the same code the WASM blob links — no TinyGo required to run `go test`. - `dummy_host_test.go` — Go-side fake host bus that mirrors the wazero dispatcher path, proving the wire format the plugin speaks matches what `packages/go/plugins/abi/hooks/marshal.go` produces. Canonical copy at `packages/go/plugins/internal/_seo_dummy.go` (leading underscore so Go skips it for builds; doc-only). - `seo_test.go` — manifest validates against `packages/go/plugins/manifest/schema.json`; unit tests for all domain functions; HTML escaping; JSON-LD parses + carries Article required fields; pinned-score table; end-to-end filter run. - `docs/04-seo-plugin-tutorial.md` — a 30-minute "build this plugin from scratch" walkthrough that mirrors the implementation file by file. - `go.work` — adds `./examples/plugins/seo` so the example builds alongside the workspace; the example has its own `go.mod` so TinyGo doesn't drag in the host's Postgres/Redis/Asynq deps. Capability rationale (the plugin uses every plumbed capability so the install dialog has a real-world cap list to render): | Capability | Why it's needed | | ----------------- | ------------------------------------------------------------ | | `posts.read` | Read the post being saved when computing the SEO score. | | `posts.write` | Persist score to `_seo_score` post meta (via host call). | | `hooks.subscribe`| Required to register the three hook subscriptions. | | `jobs.enqueue` | Required to enqueue `seo.recompute-scores`. | ## Test plan - [x] `go test -race -count=1 ./examples/plugins/seo/...` — 14 tests pass (manifest schema, unit tests, JSON-LD validity, score table, end-to-end filter) - [x] `cd packages/go && go test -race -count=1 ./plugins/lifecycle/... ./plugins/abi/hooks/...` — pass (runtime regression check per the brief) - [x] `cd packages/go && go test -race -count=1 ./plugins/manifest/...` — pass - [x] `bash examples/plugins/seo/build.sh` — exits with a helpful "tinygo not on PATH" message when toolchain missing; README documents the install URL - [ ] Manual: install TinyGo, run `./build.sh`, pack into `seo.gnplugin` via `zip -j seo.gnplugin manifest.json seo.wasm`, install + activate via the lifecycle Manager (deferred — requires the admin install dialog from #350) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Signed-off-by: Mohamed Tayeb Mokni <tayeb.mokni@gmail.com> Co-authored-by: Mohamed Tayeb Mokni <tayeb.mokni@gmail.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
packages/go/plugins/runtime/— the wazero-backed WebAssemblyhost that loads plugin
.wasmmodules, exposes a minimal host ABI(
gn_log,gn_panic,gn_time_ms), and lets host code invokeexports.
sync.Mutexserializes calls (wazero's
api.Function.Callis documented asnon-thread-safe per instance — pools that want true parallelism
hold N instances).
WithHostModuleextension seam lets future packages (capabilityABI Capability registry + ed25519 token gate enforced on every host call #107) register their own host modules without modifying this
package. A placeholder 16 MiB memory cap stands in until real
resource limits ship in Plugin host: per-invocation resource limits (memory, fuel, wall-clock, host-call counters) #15.
Closes #6.
What's NOT here (separate issues)
http.outbound,kv.*,db.query, …) — Capability registry + ed25519 token gate enforced on every host call #107The package's
doc.godocuments the seams each of those plugs into.Test plan
go vet ./plugins/runtime/...cleango test -race -count=1 ./plugins/runtime/...— 17 tests passadd(7, 35) == 42after LoadModulegn_panicsurfaces as*TrapErrorcarryingthe guest message
*CompileError(no host panic, no leak)-race, all return correct results(memory 1024)(64 MiB) isrejected by the 256-page (16 MiB) runtime cap with a clean
error (no panic)
Closeis idempotent; in-flightCalldrains before closereturns
WithHostModulebuilder is invoked and propagates errorsNotes for reviewers
The hand-authored WASM bytes in
testdata_test.goare annotatedsection-by-section. The matching human-readable
.watsources livein
plugins/runtime/wat/alongside a Makefile that rebuilds themwhen
wat2wasmis installed locally. The committed.wasmbytesare the source of truth so
go testhas no on-disk dependency.gn_panicsurfaces guest messages via a package-level recorder mapkeyed by module name. This is the simplest way to get a custom
payload out of a host function that has to close the module to
trigger the trap. The mechanism is documented in
module.go(
classifyCallError) andhost.go(hostGnPanic).🤖 Generated with Claude Code