wasm

package
v0.3.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 4, 2026 License: GPL-3.0 Imports: 19 Imported by: 0

Documentation

Overview

Package wasm is Ether's hosted plugin runtime: it runs plugin code inside the Ether process, sandboxed, with no ports and no operating-system access. SPEC section 5.2 is the contract.

Three kinds of hosted plugin are supported, all pure Go (CGO_ENABLED=0):

  • JavaScript, on QuickJS-NG compiled to WebAssembly and executed by wazero (via github.com/fastschema/qjs; see THIRD_PARTY_NOTICES.md).
  • TypeScript, compiled to JavaScript by Ether with esbuild's Go API (see Compile), then run as JavaScript.
  • Raw WebAssembly modules, executed directly by wazero.

Request ABI (all kinds)

A plugin answers HTTP-shaped JSON requests. The request is {"method","path","query","headers","body"} and the response is {"status","headers","body"}. In JavaScript the plugin defines handle(request) -> response with plain objects (the TypeScript SDK's plugin({...}) router produces this); host services are on the ether global (ether.kv.get, ether.http.fetch, ether.log, ether.abort, ...).

Raw WASM ABI

A raw module must export "memory", "alloc" and "handle":

alloc(size i32) -> i32                     // guest allocator; host writes input here
handle(ptr i32, len i32) -> i64            // returns (outPtr<<32)|outLen

Input and output are the request/response JSON above, UTF-8, in linear memory. The host copies the output before the next call, so the guest may reuse its buffers. Optional hot-path exports:

rank_key(candPtr, candLen, cfgPtr, cfgLen i32) -> i64
cond_match(candPtr, candLen, opPtr, opLen, valPtr, valLen i32) -> i32 (0 or 1)

where candidate/config/value are JSON and operator is a plain string. Allowed imports are only "ether.log(ptr,len)", "ether.abort(code i32)" (never returns) and "ether.host_call(namePtr,nameLen,argsPtr,argsLen,outPtr,outCap i32) -> i32" which runs a host service named like "kv.get" with a JSON argument array, writes the JSON result into outPtr and returns its length, or a negative error code: -1 error, -2 forbidden, -3 governance (unit budget), -4 the result did not fit in outCap (the return is then -(needed) if needed > 4, see ErrHostResultTooBig handling in raw.go). Any other import (WASI included) makes the module refuse to load. On the hot path host_call always returns -2: ranking code may only log and abort.

Governance

Ported from appkit-sandbox semantics. Every call has a wall deadline, a memory cap (default 32 MiB, maximum 64 MiB) and a unit budget. Units are charged per host call from a cost table plus 1 unit per 100us of wall time, checked at host calls and at return. Outcomes are classified as ok, error, timeout, forbidden, aborted or governance; traps and timeouts poison the instance (it is discarded, never reused) and are counted in Stats so the plugin host can apply backoff and disable a failing plugin.

Hot path

RankBatch and CondMatch run ranking steps and rule conditions in separate instances with no host services, a deterministic environment (Date fixed at the epoch, Math.random a seeded generator reset per batch), a default budget of 2ms per candidate and 50ms per batch, neutral results (rank 0, condition false) on exhaustion, and memoization keyed by (plugin id, version, config hash, release id).

The budgets bound the plugin's own time. Handing over the facts is host work and is not charged. Time is read from the monotonic clock and, when that says a budget is spent, from the thread's CPU clock, so time the thread spent descheduled on a busy host is not charged either. A batch stops starting candidates when its budget is spent; what it finished is memoized, so the next batch for the same releases continues from there. Separately, a hard deadline of twice the batch budget (Limits.hotKill) kills an instance whose code stops returning. The budgets are stated in reference-machine time; the engine measures its own speed once per process and scales them (see HotScale), so a slow CPU or a race-detector build does not cost a light plugin its ranking.

Index

Constants

View Source
const (
	// DefaultRankPerCandidate and DefaultRankBatch are the hot-path budgets
	// (SPEC §5.2). A manifest may lower them, never raise them.
	DefaultRankPerCandidate = 2 * time.Millisecond
	DefaultRankBatch        = 50 * time.Millisecond
	// WallUnitInterval is the wall time worth one unit.
	WallUnitInterval = 100 * time.Microsecond
	// BaseCallUnits is charged once per handle call.
	BaseCallUnits = 10
	// MaxMemoryBytes is the hard ceiling for a plugin instance.
	MaxMemoryBytes = 64 << 20
	// MaxResponseBytes caps a plugin response.
	MaxResponseBytes = 8 << 20
	// MaxHostResultBytes caps one host-call result returned to a guest.
	MaxHostResultBytes = 4 << 20
)

Variables

View Source
var DefaultCosts = map[string]int64{
	"log": 1, "kv.get": 5, "kv.put": 10, "kv.delete": 5, "kv.list": 10,
	"secret.get": 5, "http.fetch": 100, "addon.get": 100, "catalog.search": 50, "catalog.get": 20,
	"viewers.history": 50, "viewers.setWatchState": 50, "collections.upsert": 50,
	"folders.list": 20, "folders.stat": 20, "notify.owner": 20, "settings.get": 5,
}

Default host-call unit costs, in the spirit of appkit-sandbox's priced host functions. Wall time adds 1 unit per WallUnitInterval.

Functions

func HotScale

func HotScale() float64

HotScale is how many times slower than the reference machine this process runs hosted plugin code (1 on the reference machine or faster). The first call measures it; it costs a few milliseconds and runs off the hot path, at plugin load.

Types

type Candidate

type Candidate struct {
	ReleaseID string
	Facts     json.RawMessage
}

Candidate is one release on the hot path. Facts is the CandidateFacts JSON (SPEC 5.8); ReleaseID keys the memo and must be stable per release.

type Diagnostic

type Diagnostic struct {
	File     string `json:"file,omitempty"`
	Line     int    `json:"line,omitempty"`   // 1-based
	Column   int    `json:"column,omitempty"` // 1-based
	Text     string `json:"text"`
	Severity string `json:"severity"` // "error" or "warning"
}

Diagnostic is one esbuild error or warning, flattened for Studio and install reporting.

func Compile

func Compile(files map[string]string, entry string) (js []byte, sourcemap []byte, diagnostics []Diagnostic)

Compile bundles TypeScript (or JavaScript) plugin sources into one IIFE JavaScript file plus an external source map, using esbuild's Go API. Files maps slash paths ("src/index.ts") to contents; entry is one of those paths. Relative imports resolve inside the map only: a plugin cannot reach the filesystem or node_modules. Exports land on the __plugin global, which the JS runtime normalizes onto globalThis, so `export function handle(...)` and `globalThis.handle = ...` both work. A .js entry that is already a plain script (a prebuilt bundle such as an installed plugin's dist/plugin.js: no import, export or CommonJS module use) is checked and returned unchanged; wrapping it again would hide its globals inside a second IIFE. On failure js is nil and diagnostics carries every error.

type Engine

type Engine struct {
	// contains filtered or unexported fields
}

Engine compiles and caches guest code and builds Plugins. It owns the wazero runtimes used by raw modules; JavaScript instances manage their own (QuickJS) modules. Safe for concurrent use.

func NewEngine

func NewEngine(logger *slog.Logger) *Engine

func (*Engine) Close

func (e *Engine) Close() error

func (*Engine) LoadJS

func (e *Engine) LoadJS(id, version, source string, host Host, lim Limits) (*Plugin, error)

LoadJS loads a JavaScript plugin (TypeScript authors go through Compile first). A probe instance is built eagerly so syntax errors and a missing handle surface at load.

func (*Engine) LoadWASM

func (e *Engine) LoadWASM(id, version string, module []byte, host Host, lim Limits) (*Plugin, error)

LoadWASM compiles (and caches) a raw module and returns the Plugin. A probe instance is built eagerly so load-time failures (bad imports, missing exports) surface here, not on first call.

type Error

type Error struct {
	Status  Status
	Message string
	Trap    bool // the instance trapped (panic in guest) or was killed by a deadline
}

Error is the error type returned by Plugin calls. It carries the Outcome so WS7's health loop can read the status, trap flag and cost from one value.

func (*Error) Error

func (e *Error) Error() string

type Fault

type Fault struct {
	Code    string
	Message string
}

Fault is a classified, non-ambient failure. A Host returns a *Fault to make the call end with that status; Code is one of the Status values other than ok (usually forbidden), or an application code the plugin can catch.

func Forbidden

func Forbidden(message string) *Fault

Forbidden builds a forbidden Fault, e.g. for a revoked permission.

func (*Fault) Error

func (f *Fault) Error() string

type Host

type Host interface {
	CallHost(ctx context.Context, name string, args json.RawMessage) (json.RawMessage, error)
}

Host is the plugin-to-Ether service surface (SPEC 5.1.5). Name is a dotted service method: kv.get, kv.put, kv.delete, kv.list, secret.get, http.fetch, catalog.search, catalog.get, viewers.history, viewers.setWatchState, collections.upsert, folders.list, folders.stat, notify.owner, log. Args is a JSON array of positional arguments; the result is any JSON value. Return a *Fault to classify the failure (forbidden for a missing or revoked grant).

type HostFunc

type HostFunc func(ctx context.Context, name string, args json.RawMessage) (json.RawMessage, error)

HostFunc adapts a function to Host.

func (HostFunc) CallHost

func (f HostFunc) CallHost(ctx context.Context, name string, args json.RawMessage) (json.RawMessage, error)

type Limits

type Limits struct {
	MemoryBytes uint64        // per instance; default 32 MiB, max 64 MiB
	Deadline    time.Duration // per handle call; default 30s
	UnitBudget  int64         // per handle call; default 10000
	PoolSize    int           // instances per pool; default 2
	// Hot-path budgets. A manifest may lower these, never raise them:
	// normalize clamps to the defaults below.
	RankPerCandidate time.Duration // default (and max) 2ms
	RankBatch        time.Duration // default (and max) 50ms
	Costs            map[string]int64
	// contains filtered or unexported fields
}

Limits bound one plugin. Zero fields take the defaults in normalize.

type Outcome

type Outcome struct {
	Status         Status
	Units          int64 // host-call units plus wall-time units
	Duration       time.Duration
	Trap           bool
	SlowCandidates int // hot path: candidates that went neutral on budget
	MemoHits       int // hot path: candidates served from the memo
}

Outcome accounts for one finished call.

type Plugin

type Plugin struct {
	// contains filtered or unexported fields
}

Plugin is one loaded hosted plugin. Call serves the request ABI; RankBatch and CondMatch serve the hot path. Safe for concurrent use.

func (*Plugin) Budgets

func (p *Plugin) Budgets() (perCandidate, perBatch time.Duration)

Budgets reports the plugin's effective hot-path budgets: the manifest's (lowered, never raised), scaled by the engine's measured speed.

func (*Plugin) Call

func (p *Plugin) Call(ctx context.Context, req Request) (Response, error)

Call runs the request ABI. The returned error, when non-nil, is a *Error classified for the host's health loop.

func (*Plugin) CallDetailed

func (p *Plugin) CallDetailed(ctx context.Context, req Request) (Response, Outcome, error)

CallDetailed is Call plus the Outcome (units, duration, trap flag).

func (*Plugin) Close

func (p *Plugin) Close() error

func (*Plugin) CondBatch

func (p *Plugin) CondBatch(ctx context.Context, cands []Candidate, operator string, value json.RawMessage) ([]bool, Outcome, error)

CondBatch checks a batch without memoization, suitable for conformance testing. It applies the same per-candidate and batch limits as ranking.

func (*Plugin) CondMatch

func (p *Plugin) CondMatch(ctx context.Context, cand Candidate, operator string, value json.RawMessage) (match bool, out Outcome, err error)

CondMatch evaluates one rule condition. Budget exhaustion and errors read as false with the reason in the Outcome, matching RankBatch's neutral-on-trouble rule.

func (*Plugin) HasCondMatch

func (p *Plugin) HasCondMatch() bool

func (*Plugin) HasHandle

func (p *Plugin) HasHandle() bool

HasHandle, HasRankKey and HasCondMatch report the exported entry points.

func (*Plugin) HasRankKey

func (p *Plugin) HasRankKey() bool

func (*Plugin) ID

func (p *Plugin) ID() string

ID and Version identify the plugin for memo keys and logs.

func (*Plugin) RankBatch

func (p *Plugin) RankBatch(ctx context.Context, cands []Candidate, config json.RawMessage) (scores []int64, out Outcome, err error)

RankBatch scores candidates with the plugin's rank_key. Scores align with cands; a candidate that exceeds its per-candidate budget, or that the batch did not reach before its budget was spent, scores the neutral 0, counts as slow and is not memoized, so a later batch for the same releases picks up where this one stopped. Errors are returned only for instance-level failures (trap, kill at the hard deadline); per-candidate slowness never fails the resolve.

func (*Plugin) Stats

func (p *Plugin) Stats() Stats

func (*Plugin) Version

func (p *Plugin) Version() string

type Request

type Request struct {
	Method  string              `json:"method"`
	Path    string              `json:"path"`
	Query   map[string][]string `json:"query,omitempty"`
	Headers map[string]string   `json:"headers,omitempty"`
	Body    json.RawMessage     `json:"body,omitempty"`
}

Request is the HTTP-shaped plugin request (SPEC 5.1.2 / 5.2).

type Response

type Response struct {
	Status  int               `json:"status"`
	Headers map[string]string `json:"headers,omitempty"`
	Body    json.RawMessage   `json:"body,omitempty"`
}

Response is the plugin's answer. Body is the raw JSON the plugin produced as its body field (usually a JSON string), passed through untouched.

type Stats

type Stats struct {
	Calls          int64
	Errors         int64
	Timeouts       int64
	Traps          int64
	Forbidden      int64
	Aborted        int64
	Governance     int64
	SlowCandidates int64
	Units          int64
}

Stats are the health counters WS7's backoff loop consumes.

type Status

type Status string

Status classifies a finished call, following appkit-sandbox.

const (
	StatusOK           Status = "ok"
	StatusInvalidInput Status = "invalid_input"
	StatusError        Status = "error"
	StatusTimeout      Status = "timeout"
	StatusForbidden    Status = "forbidden"
	StatusAborted      Status = "aborted"
	StatusGovernance   Status = "governance"
)

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL