HIP-0106's plugin contract, in the framework rather than in 116 repositories. Declaration projects the LIVE ROUTER — every method+pattern, complete by construction, so a route a plugin serves and does not publish is still declared. That is the analytics-ingestion outage class: the published paths were listed, the four ingestion doors were not, and every product beacon in the fleet answered 405. Declaration/Route/(*App).Declaration what a host discovers /.well-known/zip/plugin.json the same value off a running child (*App).Described `<bin> openapi <f>` / `<bin> declare <f>` Tenant the tenancy read, with the real rule Delegate a continuation for another org Ask dial a peer by name, invoke one op (*App).Peer peer ops on their own app, own socket (*App).Ops /healthz + /readyz + /metrics Config.Eager the one fact a router cannot show App.claim a second claim of a prefix is an ERROR zip/runtime -> zip/js a language runtime is an adaptor Four things the reference plugin falsified while being built: - Describe was already taken. zip.Describe(methodPath, Doc) is the doc registry generated code calls. The projection writer is (*App).Described, one call, because argv is read in one place and a main cannot forget to forward it. - zip's control plane is NOT all under /.well-known/. /docs and the MCP path are not, so a prefix filter let a child declare them — paths outside /v1/ that every plugin serves and whose first claimant takes the HOST's document and agent door. App.control now registers and records in one call, so leaking one is impossible rather than unlikely. - OPS_PORT appeared in no Go file in the fleet. HIP-0119 named the paths and nobody implemented them, so conformance meant 116 repos hand-rolling three routes and a listener. It is one sibling app now, brought up by Listen. - There is deliberately NO default ops port. A default made every second app in a process die on "address already in use" — a plugin has two apps in it and a test process has many. The deployment states OPS_PORT; a host states none on a child, so a child correctly owns none. Load's duplicate claim used to return nil while the first registration kept answering: the losing plugin's whole surface served another plugin's 404 and nothing said so. It is now an error naming both claimants, all-or-nothing across a plugin's prefixes, taken before anything is spawned. a.ops was the op registry and is now a.registry — the ONE registry the OpenAPI document, the MCP tool, the CLI command and the plane op are all projected from. a.ops is the ops sibling. Co-authored-by: Hanzo Dev <[email protected]>
zip/js — embedded multi-language code runner
runtime embeds language backends into a zip service so a request can run
arbitrary-language source in-process — no separate runtime service, no
inter-service RPC, no container-per-language.
Runner
Runner is a registry of language → Engine plus a goroutine-scoped,
context.Context-bound dispatch loop:
type Runner interface {
Run(ctx context.Context, lang string, src []byte, args ...any) (any, error)
Register(lang string, engine Engine) error
Languages() []string
}
func NewRunner() Runner
Each Run call:
- Looks up the engine for
lang. No engine →ErrUnknownLanguage. - Spawns a worker goroutine that calls
engine.Eval(ctx, src, args...). - Spawns a watcher goroutine on
ctx.Done()that, for anInterruptibleengine, callsInterrupt(ctx.Err())so a runaway eval unwinds instead of leaking. - Joins both goroutines before returning, so a borrowed VM is never
handed back to its pool while still interrupted. Returns the engine
result, or
ctx.Err()if cancellation won the race.
This generalizes the settled JSRuntime.EvalContext contract (one worker,
one watcher, joined before return, engine-specific interrupt on cancel)
across every backend a host registers.
Engine SPI
// Implemented by every backend.
type Engine interface {
Eval(ctx context.Context, src []byte, args ...any) (any, error)
}
// Optional: backend whose running eval can be aborted out-of-band
// (goja vm.Interrupt, pyvm PyErr_SetInterrupt, wasm trap).
type Interruptible interface {
Interrupt(cause error)
}
An engine that fully manages cancellation inside Eval (zip's goja engine
does — EvalContext owns the per-call interrupt watcher and targets only
the borrowed VM) marks itself so the Runner's watcher is a benign no-op and
the not-interruptible warning is suppressed. An engine that is neither
Interruptible nor self-managed gets a one-time registration warning: its
Run still returns ctx.Err() promptly, but the worker runs to completion.
Dependency direction — the registry is the seam, not an import edge
zip does not import hanzoai/base. Engine here is a zip-side
projection of base's extruntime backends; any value implementing Eval
satisfies it. base imports zip and registers its backends at app startup —
zip exposes the registry and never names base:
// In the host binary (which already imports base) — NOT in zip:
runner := zipruntime.NewRunner()
// zip ships exactly one in-tree engine: pure-Go goja, no cgo.
js, _ := zipruntime.NewJSRuntime(zipruntime.JSOptions{PoolSize: 8})
runner.Register("js", js.Engine())
// base's backends plug in through the same seam via thin extruntime
// adapters — zip stays base-free:
runner.Register("py", pyvmEngine()) // base/plugins/pyvm (cgo CPython)
runner.Register("js-v8", v8vmEngine()) // base/plugins/v8vm (cgo V8)
runner.Register("wasm", wasmvmEngine()) // base/plugins/wasmvm (pure-Go wazero)
runner.Register("stark", starkvmEngine()) // base/plugins/starkvm
gojavm (base/plugins/gojavm) is the canonical first backend: it wraps
this package's *JSRuntime, so there is exactly one goja engine in the
stack and base, cloud, and every other zip consumer share it.
See also
jsvm.go— pooled goja VMs +EvalContextcancellation contract.esbuild.go— TS/modern-JS → ES5 for the goja engine.examples/express-in-zip/main.go—POST /runtime/:langmounts the Runner over HTTP.