Wasm Plugin Checkers
ServiceRadar supports sandboxed Wasm-based plugins for custom checkers. Plugins are uploaded or imported through the web UI, reviewed for capabilities and allowlists, and then assigned to agents. Agents run plugins in an embedded Wasm runtime with strict resource limits and a capability-based host ABI.
The current edge model is push-based: the agent streams results to agent-gateway. External "pull" checkers are not part of the primary architecture; prefer Wasm plugins or first-party collectors that publish into the normal pipelines.
Wasm is also how ServiceRadar ships certain first-party checks. For example, the Dusk checker runs as a Wasm plugin executed by serviceradar-agent (it is not a standalone service).
Wasm plugins are one part of the edge runtime. The agent also runs embedded engines (sync integrations, SNMP polling, discovery/mapping, mDNS) alongside plugins.
Package Format
Each plugin package is made up of:
plugin.yaml(manifest)plugin.wasm(Wasm binary)- optional config schema (JSON Schema)
The control plane stores the manifest and config schema in the database and stores the Wasm binary in the configured package storage backend.
Manifest fields
Required fields:
id: stable plugin identifiername: human-readable nameversion: semver stringentrypoint: exported Wasm function name (no args)capabilities: list of host function capabilitiesresources: requested resource budgetoutputs: must beserviceradar.plugin_result.v1
Optional fields:
descriptionruntime:wasi-preview1ornonepermissions: allowlists for HTTP/TCP/UDPsource: metadata such as repo URL, commit, licenseschema_version: UI schema version for config/result contracts (default1)display_contract: supported result widgets (optional)
Example plugin.yaml:
id: http-check
name: HTTP Check
version: 1.0.0
description: Simple HTTP health check
entrypoint: run_check
runtime: wasi-preview1
outputs: serviceradar.plugin_result.v1
schema_version: 1
capabilities:
- get_config
- log
- submit_result
- http_request
permissions:
allowed_domains:
- api.example.com
allowed_ports:
- 443
resources:
requested_memory_mb: 64
requested_cpu_ms: 2000
max_open_connections: 4
display_contract:
schema_version: 1
widgets:
- status_badge
- stat_card
- table
- markdown
- sparkline
source:
repo_url: https://github.com/acme/http-check
commit: 0123456
Config schema
Plugins can include a JSON Schema document describing their runtime configuration. The schema is stored with the package and used for validation in the UI.
Example schema:
{
"type": "object",
"properties": {
"url": {"type": "string"},
"timeout_ms": {"type": "integer", "minimum": 1}
},
"required": ["url"]
}
Supported JSON Schema subset:
- Root schema MUST be
type: object. - Supported keywords:
type,title,description,default,enum,minimum,maximum,minLength,maxLength,pattern,format,items,properties,required,additionalProperties. - Supported formats:
uri,email. - Array fields MUST define
items.
Result Schema (serviceradar.plugin_result.v1)
Plugins submit results as JSON via the submit_result host function. The agent validates results and maps them to GatewayServiceStatus.
Required fields:
status:OK,WARNING,CRITICAL, orUNKNOWNsummary: human-readable summary
Common optional fields:
perfdata: Nagios-style perfdata stringmetrics: list of structured metricslabels: map of label keys/valuesobserved_at: RFC3339 timestamp (added by agent if omitted)schema_version: UI schema version for display instructions (default1)display: list of UI widget instructions- Each widget may include
layout: full|halfto control grid width in the Services UI.
- Each widget may include
Example result payload:
{
"status": "OK",
"summary": "http 200 in 42ms",
"perfdata": "latency_ms=42",
"metrics": [
{"name": "latency_ms", "value": 42, "unit": "ms"}
],
"labels": {
"target": "api.example.com"
},
"schema_version": 1,
"display": [
{"widget": "stat_card", "label": "Latency", "value": "42ms", "tone": "success"},
{"widget": "table", "data": {"Status": "200", "Region": "us-east-1"}, "layout": "full"}
]
}
Capabilities and Permissions
Capabilities are explicitly declared in the manifest and approved during import review. The agent enforces both the capability list and the permission allowlists on every host call.
Common capabilities:
get_config: retrieve assignment parameterslog: emit structured logssubmit_result: send a plugin result payloadhttp_request: perform HTTP through the host proxytcp_connect/tcp_read/tcp_write/tcp_closeudp_sendto
Permissions:
allowed_domains: HTTP hostname allowlist (supports*and*.suffix)allowed_networks: CIDR allowlist for TCP/UDPallowed_ports: TCP/UDP port allowlist
SDK and Authoring
Plugins compile to wasm32-wasi and export a zero-argument entrypoint function that matches the manifest entrypoint. Host functions are imported from the env module.
SDKs:
- Go SDK:
carverauto/serviceradar-sdk-go - Rust SDK: planned (not yet generally available)
Go (TinyGo) With The ServiceRadar SDK
If you're writing plugins in Go, use the Go SDK repo: carverauto/serviceradar-sdk-go.
This gives you a higher-level API over the host ABI:
sdk.Execute(func() (*sdk.Result, error) { ... })for structured execution + error handlingsdk.LoadConfig(&cfg)to decode assignment config JSONsdk.HTTP/sdk.TCPDial/sdk.UDPSendTowrappers (proxy + allowlists enforced by the agent)- result builders (
sdk.NewResult(),sdk.Ok(), metrics, labels, widgets)
Example: HTTP Latency Check (Go SDK)
main.go:
//go:build tinygo
package main
import (
"fmt"
"github.com/carverauto/serviceradar-sdk-go/sdk"
)
type Config struct {
URL string `json:"url"`
WarnMS float64 `json:"warn_ms"`
CritMS float64 `json:"crit_ms"`
}
//export run_check
func run_check() {
_ = sdk.Execute(func() (*sdk.Result, error) {
cfg := Config{URL: "https://example.com/health"}
_ = sdk.LoadConfig(&cfg)
resp, err := sdk.HTTP.Get(cfg.URL)
if err != nil {
res := sdk.Critical("http request failed")
res.EmitEvent(sdk.SeverityCritical, "http request failed", "http_request_failed")
res.RequestImmediateAlert("http_request_failed")
return res, nil
}
latencyMS := float64(resp.Duration.Milliseconds())
thresholds := sdk.Thresholds(cfg.WarnMS, cfg.CritMS)
res := sdk.NewResult()
res.SetSummary(fmt.Sprintf("http %d in %.0fms", resp.Status, latencyMS))
res.ApplyThresholds(latencyMS, thresholds.Warn, thresholds.Crit)
res.AddMetric("latency_ms", latencyMS, "ms", thresholds)
res.AddStatCard("Latency", fmt.Sprintf("%.0fms", latencyMS), toneForStatus(res.Status))
return res, nil
})
}
func main() {}
func toneForStatus(status sdk.Status) string {
switch status {
case sdk.StatusOK:
return "success"
case sdk.StatusCritical:
return "critical"
case sdk.StatusWarning:
return "warning"
case sdk.StatusUnknown:
return "neutral"
default:
return "success"
}
}
plugin.yaml:
id: http-check
name: HTTP Check
version: 0.1.0
entrypoint: run_check
outputs: serviceradar.plugin_result.v1
capabilities:
- get_config
- log
- submit_result
- http_request
resources:
requested_memory_mb: 64
requested_cpu_ms: 2000
permissions:
allowed_domains:
- example.com
allowed_ports:
- 443
Build with TinyGo:
tinygo build -o plugin.wasm -target=wasi ./
More examples live in the SDK repo under examples/:
examples/http-checkexamples/tcp-checkexamples/udp-checkexamples/widgets-check
Minimal Host ABI Example (No SDK)
If you want to avoid the SDK, you can use direct host imports.
Minimal TinyGo example:
package main
import "unsafe"
//go:wasmimport env submit_result
func hostSubmitResult(ptr uint32, size uint32) int32
//export run_check
func run_check() {
payload := []byte(`{"status":"OK","summary":"hello from wasm"}`)
if len(payload) == 0 {
return
}
ptr := uint32(uintptr(unsafe.Pointer(&payload[0])))
hostSubmitResult(ptr, uint32(len(payload)))
}
func main() {}
Notes:
- Use TinyGo or Rust with a WASI target.
- The entrypoint takes no arguments.
- JSON payloads are required for
submit_resultandhttp_request. - The agent enforces resource limits (memory, CPU time, max connections).
HTTP request payload shape
The http_request host function expects a JSON request and writes a JSON response:
{
"method": "GET",
"url": "https://api.example.com/health",
"headers": {"accept": "application/json"},
"timeout_ms": 2000
}
Response:
{
"status": 200,
"headers": {"content-type": "application/json"},
"body_base64": "eyJzdGF0dXMiOiJvayJ9",
"body_encoding": "base64"
}
Upload and Import Workflow
- Upload or import a plugin package in the admin UI.
- The package is staged and must be approved.
- During review, confirm capabilities, permissions, and resource requests.
- Approved packages can be assigned to agents.
- Agents download packages only from the ServiceRadar control plane (never directly from GitHub).
GitHub imports and verification
For GitHub-sourced plugins, the control plane fetches:
plugin.yamlplugin.wasm- optional config schema
Commit verification is captured from GitHub. If PLUGIN_REQUIRE_GPG_FOR_GITHUB=true, unsigned or unverified commits are rejected during import.
Deployment Configuration
Wasm packages are served by the web-ng API and stored using a configurable backend. For production, store plugin blobs on persistent storage and back them up with normal platform operations.
Filesystem backend (default)
- Storage path:
/var/lib/serviceradar/plugin-packages - Configure with:
PLUGIN_STORAGE_BACKEND=filesystemPLUGIN_STORAGE_PATH=/var/lib/serviceradar/plugin-packagesPLUGIN_STORAGE_SIGNING_SECRET(shared with core for signed download URLs)
Docker:
- Mount a volume to
/var/lib/serviceradar/plugin-packagesin theweb-ngcontainer.
Kubernetes:
- Mount a PVC at
/var/lib/serviceradar/plugin-packagesfor theweb-ngdeployment.
Core download URLs:
PLUGIN_STORAGE_PUBLIC_URL(base URL for web-ng, e.g.https://staging.serviceradar.cloud)PLUGIN_STORAGE_SIGNING_SECRET(must match web-ng)PLUGIN_STORAGE_DOWNLOAD_TTL_SECONDS(default 86400)
JetStream object store
Set:
PLUGIN_STORAGE_BACKEND=jetstreamPLUGIN_STORAGE_BUCKET=serviceradar_pluginsPLUGIN_STORAGE_JS_MAX_BUCKET_BYTESPLUGIN_STORAGE_JS_MAX_CHUNK_BYTESPLUGIN_STORAGE_JS_REPLICASPLUGIN_STORAGE_JS_STORAGE(fileormemory)PLUGIN_STORAGE_JS_TTL_SECONDS
This backend requires NATS JetStream to be available to web-ng.
GitHub access and verification policy
GITHUB_TOKENorGH_TOKENfor private reposPLUGIN_REQUIRE_GPG_FOR_GITHUB=trueto reject unverified commitsPLUGIN_ALLOW_UNSIGNED_UPLOADS=falseto require signatures for uploads
Operational Tips
- Keep per-agent engine limits conservative and override down in assignments if needed.
- Use the Settings -> Agent capacity view to confirm headroom before assignments.
- Store plugin source details in the manifest
sourcesection for auditability.