diff --git a/README.md b/README.md
index a46bc7a..4ef5722 100644
--- a/README.md
+++ b/README.md
@@ -1,339 +1,199 @@
-
component-model
-
A capability-gated WebAssembly Component Model plugin for the Wago runtime.
+
component-model
+
WebAssembly Component Model execution and Canonical ABI linking for Wago.
-
-
-
+
+
-
-Table of Contents
-
-- [Overview](#overview)
-- [Installation](#installation)
-- [Concepts](#concepts)
-- [Usage](#usage)
- - [Running a component](#running-a-component)
- - [Providing host imports](#providing-host-imports)
- - [Depending on the runtime service](#depending-on-the-runtime-service)
-- [API](#api)
- - [Runtime and instances](#runtime-and-instances)
- - [WIT types and host imports](#wit-types-and-host-imports)
- - [Resources](#resources)
- - [Async calls](#async-calls)
- - [Compile cache](#compile-cache)
-- [Security](#security)
-- [Compatibility](#compatibility)
-- [Testing](#testing)
-- [Architecture](#architecture)
-- [Contributing](#contributing)
-- [License](#license)
-- [Contact](#contact)
-
-
-
-## Overview
-
-`component-model` is an optional [Wago](https://github.com/wago-org/wago) plugin for
-decoding, linking, and executing WebAssembly Components. Core Wago remains a compact
-core-Wasm runtime; applications that never select this plugin do not retain the
-component implementation in their Go dependency graph.
-
-What you get out of the box:
-
-- **Component execution**: decode real component binaries, compile their embedded core
- modules, resolve nested instantiation graphs, and call typed exports.
-- **Canonical ABI**: lift and lower primitive and composite WIT values, strings, lists,
- variants, results, options, flags, resources, streams, and futures.
-- **Typed host linking**: implement component imports with Go functions and an explicit
- WIT type vocabulary.
-- **Safe composition**: component-world plugins such as WASI consume a versioned service
- instead of receiving core-runtime authority themselves.
-- **Explicit lifecycle**: component instances, resources, async calls, compile caches,
- and the underlying engine handle have defined ownership and shutdown paths.
-
-The plugin ID is `wago-org/component-model`. It publishes the typed service
-`wago-org/component-model/runtime/v1`.
-
-> **Stability:** experimental (`v0.0.0`). The API and supported Component Model surface
-> may change without notice.
-
-## Installation
-
-If you have the [`wago`](https://github.com/wago-org/wago) CLI installed:
+`component-model` is Wago's optional Component Model runtime. It decodes
+components, links their embedded core-module graph, implements Canonical ABI
+lift and lower, and exposes typed component exports and host imports. Core-only
+Wago programs do not link it.
-```sh
-wago pkg install github.com/wago-org/component-model
-```
+The plugin ID is `github.com/wago-org/component-model`. It provides the typed
+`github.com/wago-org/component-model/runtime` contract at major version 1.
+There is no global registration or ambient runtime lookup.
+
+The implementation currently covers Preview 2 component binaries, nested
+composition, strings and composite WIT values, resources, typed host imports,
+and experimental async tasks, futures, and streams. WASI policy stays in the
+separate [`github.com/wago-org/wasi`](https://github.com/wago-org/wasi) plugin.
+
+## Install
-or use [`go get`](https://pkg.go.dev/cmd/go#hdr-Get_packages_and_dependencies):
+Add the plugin to a Wago project:
```sh
-go get github.com/wago-org/component-model
+wago add github.com/wago-org/component-model
```
-Select the plugin in your project's `wago.json`:
+For Go development against its public types:
-```json
-{
- "$schema": "https://wago.sh/v0/schema.json",
- "plugins": {
- "wago-org/component-model": "^0.0.0"
- }
-}
+```sh
+go get github.com/wago-org/component-model
```
-Component execution requires one privileged plugin capability: `core.runtime`. Record
-the reviewed authority and exact version in `wago-lock.json`:
-
-```json
-{
- "plugins": {
- "wago-org/component-model": {
- "version": "0.0.0",
- "requiredCapabilities": ["core.runtime"],
- "capabilities": {
- "core.runtime": true
- }
- }
- }
-}
-```
+The Wago installer resolves the full dependency and contract graph, then asks
+the user to review the plugin's three exact authorities. The accepted versions,
+definition digest, grants, scopes, and contract bindings live in
+`wago-lock.json`.
-Generated Wago hosts blank-import the conventional registration package:
+Generated runtimes call `register.Providers()` explicitly. Importing the
+package does not mutate a process-global registry:
```go
-import _ "github.com/wago-org/component-model/register"
+providers := component_register.Providers()
```
-## Concepts
+## Consume the service from another plugin
-| Term | Meaning |
-| --- | --- |
-| **Plugin** | The `component.Extension` installed under `wago-org/component-model`. It receives the narrow core-engine capability and provides one service. |
-| **Service** | `component.RuntimeService`, the typed `wago-org/component-model/runtime/v1` dependency used by WASI and other component-world plugins. |
-| **Runtime** | A `*component.Runtime` bound to one Wago runtime. It instantiates components but cannot register plugins, inspect policy, or control the Wago runtime lifecycle. |
-| **Instance** | A live component graph with typed exports, embedded core instances, resources, and explicit `Close`. |
-| **Option** | Host imports, WIT signatures, resource mappings, host state, or a compile cache supplied to one instantiation. |
+A component-world plugin declares both its package dependency and its contract
+dependency. The package edge selects and versions the implementation. The
+contract edge selects the typed API.
-The ownership chain is **Wago runtime → component service → component instance →
-resources and calls**. Closing the Wago runtime revokes the engine capability and typed
-service references.
+```go
+var definition = wago.PluginDefinition{
+ ID: "github.com/acme/component-world",
+ Name: "Acme Component World",
+ Version: "0.1.0",
+ Description: "Host policy for Acme components.",
+ Stability: wago.Experimental,
+ Provenance: wago.PluginProvenance{
+ Repository: "https://github.com/acme/component-world",
+ License: "Apache-2.0",
+ },
+ Requires: []wago.PluginRequirement{
+ {ID: component.PluginID, Version: "^0.1.0"},
+ },
+ Consumes: []wago.ContractRequirement{
+ {
+ ID: component.Contract.ID(),
+ Major: component.Contract.Major(),
+ Mode: wago.ContractRequired,
+ },
+ },
+}
-## Usage
+type worldPlugin struct {
+ components *plugin.Ref[component.Service]
+}
-### Running a component
+func (p *worldPlugin) Register(reg *wago.Registrar) error {
+ var err error
+ p.components, err = plugin.Require(reg, component.Contract)
+ return err
+}
+```
-`Enable` installs the registered plugin with its required engine grant and returns its
-runtime-scoped service:
+Calls use two nested callbacks. `Ref.With` leases the provider contract, and
+`Service.WithInstance` owns one component instance through its callback:
```go
-package main
-
-import (
- "context"
-
- component "github.com/wago-org/component-model"
- "github.com/wago-org/wago"
-)
-
-func run(ctx context.Context, componentBytes []byte) error {
- rt := wago.NewRuntime()
- defer rt.Close()
-
- components, err := component.Enable(rt)
- if err != nil {
- return err
- }
- instance, err := components.Instantiate(ctx, componentBytes)
- if err != nil {
- return err
- }
- defer instance.Close(ctx)
-
- _, err = instance.Call(ctx, "example:app/run#run")
- return err
+func (p *worldPlugin) run(ctx context.Context, wasm []byte) error {
+ return p.components.With(func(components component.Service) error {
+ return components.WithInstance(ctx, wasm, func(in *component.Instance) error {
+ _, err := in.Call(ctx, "wasi:cli/run@0.2.3#run")
+ return err
+ })
+ })
}
```
-World plugins supply the imports a component expects. For example, the separate
-[`wago-org/wasi`](https://github.com/wago-org/wasi) plugin owns WASI filesystem,
-network, clocks, random, environment, and process policy; this repository grants none
-of that authority.
+`WithInstance` closes the complete component graph before it returns. Do not
+retain the service or instance outside its callback. During shutdown Wago
+rejects new contract calls, waits for active callbacks and component cleanup,
+then revokes the core handles. Consumers can still call the service from their
+own `Stop` callback because Wago stops consumers before providers.
-### Providing host imports
+## Host imports
-`WithImport` covers primitive and top-level composite signatures. A host function
-receives lifted Go values and returns lifted values; returning a Go error traps the
-guest call.
+`WithImport` registers a synchronous host function with an explicit WIT
+signature:
```go
echo := func(ctx context.Context, args []component.Value) ([]component.Value, error) {
- return []component.Value{args[0]}, nil
+ return []component.Value{args[0]}, nil
}
stringType := component.PrimitiveDesc{Prim: "string"}
-instance, err := components.Instantiate(ctx, componentBytes,
- component.WithImport(
- "example:host/echo@1.0.0",
- "echo",
- echo,
- []component.TypeDesc{stringType},
- []component.TypeDesc{stringType},
- ),
+option := component.WithImport(
+ "example:host/echo@1.0.0",
+ "echo",
+ echo,
+ []component.TypeDesc{stringType},
+ []component.TypeDesc{stringType},
)
```
-For nested WIT types, build one `TypeTable` and pass its `FuncDesc` and `Resolver`
-together:
+Pass options after the callback:
```go
-types := component.NewTypeTable()
-result := types.Result(types.List(component.Prim("string")), component.Prim("u32"))
-signature := types.Func([]component.TypeRef{component.Prim("string")}, result)
-
-option := component.WithImportCustom(
- "example:host/catalog@1.0.0",
- "lookup",
- lookup,
- signature,
- types.Resolver(),
-)
+err := components.WithInstance(ctx, wasm, useInstance, option)
```
-Interface matching ignores the patch component of an `@x.y.z` version, while the
-Canonical ABI signature is still checked structurally.
+For nested WIT types, build a `TypeTable` and pass its `FuncDesc` and
+`Resolver` to `WithImportCustom`. Resource-bearing interfaces use
+`WithResourceTag`, `WithResourcesHook`, and `WithHostResourceDtor`. Async host
+functions use `WithAsyncImport`, `Instance.CallAsync`, and `PendingCall`.
-### Depending on the runtime service
+## Compile cache
-Plugins that provide component worlds should require `RuntimeService`; they should not
-request `core.runtime` themselves:
+`CompileCache` reuses component decoding and embedded core-module compilation
+across repeated calls:
```go
-type Extension struct {
- components *plugin.Ref[component.Service]
-}
-
-func (e *Extension) Register(reg *wago.Registry) (err error) {
- e.components, err = plugin.Require(reg, component.RuntimeService)
- return err
-}
+cache := component.NewCompileCache()
+defer cache.Close(context.Background())
-func (e *Extension) instantiate(ctx context.Context, wasm []byte, opts ...component.Option) (*component.Instance, error) {
- components, err := e.components.Get()
- if err != nil {
- return nil, err
- }
- return components.Instantiate(ctx, wasm, opts...)
-}
+err := components.WithInstance(
+ ctx,
+ wasm,
+ useInstance,
+ component.WithCompileCache(cache),
+)
```
-Manifest loading orders the provider before consumers automatically. Programmatic hosts
-install the provider first; service resolution is type-checked and transactional.
-
-## API
-
-### Runtime and instances
-
-- `Enable(rt)` installs the plugin and returns its `*Runtime`.
-- `FromRuntime(rt)` resolves an already-installed service and fails if it is absent or
- invalid.
-- `Runtime.Instantiate` decodes, compiles, links, and instantiates a component.
-- `Instance.Call` invokes a top-level export by name.
-- `Instance.CallExport` invokes a member of an exported component instance.
-- `Instance.Close` releases the component graph and its retained resources.
-
-Call arguments and results use the Canonical ABI's Go shapes: integer and floating-point
-scalars, `string`, `[]byte`, `[]component.Value`, `VariantValue`, `ResultValue`, and
-`uint32` resource representations.
-
-### WIT types and host imports
-
-`PrimitiveDesc`, `RecordDesc`, `VariantDesc`, `ListDesc`, `TupleDesc`, `FlagsDesc`,
-`EnumDesc`, `OptionDesc`, `ResultDesc`, `OwnDesc`, `BorrowDesc`, `StreamDesc`, and
-`FutureDesc` form the public WIT type vocabulary. `TypeTable` provides constructors for
-nested signatures and keeps their type references paired with the correct resolver.
-
-Use `WithImport` for ordinary signatures and `WithImportCustom` when a function contains
-nested composites.
-
-### Resources
-
-Resource-bearing host interfaces use explicit tags:
+A cache belongs to one loaded Component Model provider. Close every instance
+callback first, then close the cache, then close the Wago runtime.
-- `WithResourceTag` maps an imported WIT resource to a host tag.
-- `WithResourcesHook` exposes the instance's checked handle table to the host.
-- `WithHostResourceDtor` registers cleanup for owned host representations.
-- `Instance.DropResource` explicitly drops a guest-visible resource handle.
+## Authorities
-Tags and handles are instance-scoped. Cross-type, stale, and invalid handle operations
-fail instead of resolving to an unrelated host representation.
+The provider asks for three required authorities:
-### Async calls
-
-`WithAsyncImport` registers an async-lowered import. `Instance.CallAsync` starts an
-export and returns a `PendingCall`; the host completes imports through `AsyncCall`, then
-awaits or cancels the export through `PendingCall.Await` or `PendingCall.Cancel`.
-
-The async API is experimental. Call completion, cancellation, streams, futures, and
-waitables remain bounded by the component instance lifecycle.
+| Authority | Why it is needed |
+| --- | --- |
+| `core.module.compile` | Compile core Wasm modules embedded in a component. |
+| `core.instance.instantiate` | Instantiate and own the core-module graph, within reviewed positive instance and memory limits. |
+| `core.funcref.create` | Build typed host references for Canonical ABI bridges. |
-### Compile cache
+These handles do not expose plugin registration, runtime policy, hooks, or
+arbitrary runtime lifecycle control. A user may narrow the requested positive
+instantiation limits. The published request allows 64 live core instances and
+16 GiB of aggregate declared maximum memory across them. Memoryless linker
+shims consume an instance slot but no memory budget. Components or concurrent
+callbacks that exceed either reviewed limit fail closed. The plugin has no
+configuration fields and rejects unknown configuration.
-`CompileCache` reuses compiled embedded core modules across repeated instantiations of
-the same component bytes:
+This authority model is an API boundary, not a sandbox for untrusted Go code.
+Audit every plugin source and pin the exact release compiled into a host.
-```go
-cache := component.NewCompileCache()
-defer cache.Close(ctx)
+## Public surface
-instance, err := components.Instantiate(ctx, componentBytes,
- component.WithCompileCache(cache),
-)
-```
+- `Service.WithInstance` scopes component execution and cleanup.
+- `Instance.Call` and `Instance.CallExport` invoke typed component exports.
+- `WithImport`, `WithImportCustom`, and `WithAsyncImport` define host imports.
+- `TypeTable` and the descriptor types express WIT signatures.
+- Resource options bind checked host resource tags, handles, and destructors.
+- `CompileCache` reuses decode and JIT work for one provider lifetime.
-A cache belongs to exactly one Wago runtime. It is safe for concurrent use, but must be
-closed after every instance using it has closed.
-
-## Security
-
-- **Narrow authority**: `core.runtime` exposes only compilation, instantiation, and host
- function references. It does not expose `*wago.Runtime`, extension registration,
- policy, hooks, or arbitrary lifecycle control.
-- **Revocable access**: the core-engine handle is inactive before transactional commit
- and revoked during runtime shutdown.
-- **Typed composition**: missing services, duplicate providers, type mismatches, and
- ungranted capabilities reject plugin activation before runtime mutation.
-- **Checked guest data**: component decoding, Canonical ABI layout, guest memory ranges,
- resource handles, and lifted value shapes are validated.
-- **No ambient host authority**: this plugin does not expose files, sockets, environment,
- clocks, randomness, or process control. World plugins must receive those capabilities
- explicitly.
-- **Explicit ownership**: instances, resources, pending calls, and compile caches have
- close or cancellation paths; service references fail closed after shutdown.
-
-The plugin runs as compiled Go code in the host process. Wago's plugin capability model
-limits access through its APIs; it is not a sandbox for untrusted Go source. Audit and
-pin every plugin compiled into a host.
-
-## Compatibility
-
-| Axis | Support |
-| --- | --- |
-| Wago engine | `>= 0.1.0`; the current development module pins the reviewed Wago revision in `go.mod`. |
-| Go toolchain | `>= 1.22` |
-| Plugin ID | `wago-org/component-model` |
-| Service API | `wago-org/component-model/runtime/v1` |
-| Stability | Experimental |
-
-Identity and catalog metadata live in [`wago.json`](./wago.json). Exact dependency
-versions and reviewed capability grants belong in the consuming project's
-`wago-lock.json`.
+Malformed component encodings, invalid type relationships, out-of-bounds
+memory access, bad resource ownership transfers, missing imports, and
+unsupported behavior return errors or named guest traps.
-## Testing
+## Test
```sh
go test ./...
@@ -341,55 +201,10 @@ go test -race ./...
go vet ./...
```
-The checked-in suite is self-contained and does not require `wasm-tools` at test time.
-It covers component decoding, Canonical ABI layout and values, typed host imports,
-nested composition, resources, async builtins, compile-cache reuse, capability denial,
-typed service resolution, and shutdown revocation.
-
-## Architecture
-
-- **`plugin.go`** — plugin identity, capability request, versioned service, and the
- runtime-scoped execution facade.
-- **`component.go`**, **`host.go`**, **`types.go`**, **`typetable.go`** — public component
- instances, host-linking options, WIT types, values, and signature construction.
-- **`internal/binary/`** — component binary decoding and semantic type/instantiation
- graphs.
-- **`internal/abi/`** — Canonical ABI layout plus value lift/lower and memory access.
-- **`internal/engine/`** — adapter from the narrow Wago core-engine interface.
-- **`internal/instance/`** — graph instantiation, calls, composition, resources, async
- tasks, streams, futures, and lifecycle ownership.
-- **`register/`** — blank-import shim for Wago-generated plugin hosts.
-- **`wago.json`** — plugin catalog metadata.
-
-The deep boundary is the versioned `Service`: Wago owns core-Wasm execution, this plugin
-owns the Component Model and Canonical ABI, and world plugins own guest-visible host
-policy. Keeping those layers separate preserves dead-code elimination and prevents a
-WASI or application-world plugin from inheriting runtime authority.
-
-## Contributing
-
-Contributions are welcome! Please:
-
-- Run `go test -race ./...` and `go vet ./...` before opening a pull request.
-- Add focused decoder, ABI, lifecycle, and fail-closed tests for new Component Model
- behavior.
-- Keep world policy out of this repository; WASI and application-specific interfaces
- belong in plugins that consume `RuntimeService`.
-- Do not widen `core.runtime` or bypass the typed service boundary for convenience.
-- Follow standard Go formatting (`gofmt`) and conventional commit messages.
+The repository includes component decoder, Canonical ABI, composition,
+resource, async, malformed-input, contract graph, strict-config, and shutdown
+revocation tests. Test fixtures are checked in; `wasm-tools` is not required.
## License
-This project is distributed under the [Apache License 2.0](./LICENSE). See
-[`NOTICE`](./NOTICE) for the provenance of the original decoder and Canonical ABI work.
-Work on this project is done out of passion—if you want to support it financially, you
-can donate through [GitHub Sponsors](https://github.com/sponsors/JairusSW).
-
-## Contact
-
-Please file issues at [GitHub Issues](https://github.com/wago-org/component-model/issues).
-To chat, join the [Wago Discord](https://wago.sh/discord).
-
-- **GitHub:** [https://github.com/wago-org/](https://github.com/wago-org/)
-- **Website:** [https://wago.sh/](https://wago.sh/)
-- **Discord:** [https://wago.sh/discord](https://wago.sh/discord)
+Apache-2.0. See [NOTICE](./NOTICE) for provenance.
diff --git a/component.go b/component.go
index b112430..4a16982 100644
--- a/component.go
+++ b/component.go
@@ -1,124 +1,76 @@
-// Package component runs WebAssembly Component Model components -- and the WASI
-// 0.2 (wasip2) world built on it -- through Wago's component plugin.
+// Package component runs WebAssembly Component Model binaries through Wago.
//
-// Where the core Wago package instantiates core modules, this package
-// instantiates *components*: genuine wasm32-wasip2 binaries produced by rustc,
-// wasm-tools, and friends. It decodes the component, wires its multi-module
-// graph (nested instances, canonical lift/lower of the Canonical ABI, resource
-// lifetimes). A host module such as wago-org/wasi provides the WASI 0.2
-// interfaces (wasi:cli, clocks, filesystem, io, random, sockets, http).
+// The package owns Component Model decoding, graph linking, Canonical ABI
+// lift/lower, resources, and typed host imports. Core Wasm compilation and
+// execution stay behind three reviewed Wago authorities. WASI and other world
+// policy belong in plugins that consume Contract.
//
-// Typical use: build a Runtime, instantiate a component with the WASI surface
-// wired to your stdio/filesystem/args, call an export, then Close.
+// A consuming plugin declares Contract in its PluginDefinition and acquires a
+// typed reference during registration:
//
-// r := wago.NewRuntime()
-// defer r.Close()
-// components, err := component.Enable(r)
-// if err != nil {
-// return err
-// }
+// components, err := plugin.Require(reg, component.Contract)
//
-// inst, err := components.Instantiate(ctx, componentWasm,
-// wasip2.With(wasip2.Config{Stdout: os.Stdout})...)
-// if err != nil {
-// return err
-// }
-// defer inst.Close(ctx)
+// Calls stay inside both the contract lease and the component instance's
+// lifetime:
//
-// // A wasi:cli/command component: run its entry point.
-// _, err = inst.Call(ctx, "wasi:cli/run@0.2.3#run")
+// err := components.With(func(service component.Service) error {
+// return service.WithInstance(ctx, componentWasm, func(in *component.Instance) error {
+// _, err := in.Call(ctx, "wasi:cli/run@0.2.3#run")
+// return err
+// })
+// })
//
-// Call arguments and results are Go values (uint32, int64, string, []any for
-// lists/records, and uint32 handles for resources), matching the Canonical
-// ABI's lifting of the component's WIT types.
-//
-// This API is young and, like the rest of Wago, makes no stability promise yet.
+// The service closes the instance before WithInstance returns. Neither the
+// service nor the instance may be retained outside its callback.
package component
import (
- "context"
-
"github.com/wago-org/component-model/internal/abi"
"github.com/wago-org/component-model/internal/instance"
- "github.com/wago-org/wago"
)
-// Instance is a live component instance. Call its exports with Call /
-// CallExport, and release it with Close. A wasi:http/incoming-handler component
-// also satisfies http.Handler via ServeHTTP.
+// Instance is a live component instance. It is valid only during the
+// Service.WithInstance callback that supplied it.
type Instance = instance.Instance
// PendingCall is a live CallAsync invocation, suspended awaiting external
// import completions. See Instance.CallAsync.
type PendingCall = instance.PendingCall
-// Option configures Instantiate. WithWASI and WithCompileCache produce Options.
+// Option configures one component instantiation.
type Option = instance.Option
-// CompileCache amortizes a component's decode and its embedded core modules'
-// compilation across repeated Instantiate calls of the same component bytes.
-// Safe for concurrent use. Pair one with a single Runtime and Close it when
-// done. See WithCompileCache and NewCompileCache.
+// CompileCache amortizes component decoding and embedded core-module
+// compilation across repeated Service.WithInstance calls. A cache belongs to
+// one loaded Component Model provider and must be closed before that runtime.
type CompileCache = instance.CompileCache
-// Instantiate resolves the Component Model plugin installed on r and delegates
-// to its runtime-scoped service. Call Enable once before using this compatibility
-// entry point. New code can retain the *Runtime returned by Enable and call its
-// Instantiate method directly.
-func Instantiate(ctx context.Context, r *wago.Runtime, componentBytes []byte, opts ...Option) (*Instance, error) {
- components, err := FromRuntime(r)
- if err != nil {
- return nil, err
- }
- return components.Instantiate(ctx, componentBytes, opts...)
-}
-
-// WithCompileCache reuses cache across this and future Instantiate calls of the
-// same component bytes, skipping the repeated decode + core-module compile.
+// WithCompileCache reuses cache across component instantiations.
func WithCompileCache(cache *CompileCache) Option { return instance.WithCompileCache(cache) }
-// NewCompileCache returns an empty CompileCache ready to pass to
-// WithCompileCache. Close it (CompileCache.Close) alongside the Runtime it is
-// paired with.
+// NewCompileCache returns an empty compile cache. Close it before closing the
+// Wago runtime that loaded the Component Model provider.
func NewCompileCache() *CompileCache { return instance.NewCompileCache() }
-// Value is a component-level call value: a Go value matching the Canonical
-// ABI's lifting of a WIT type (uint32, int64, float64, string, []any for
-// lists/records/tuples, uint32 for resource handles). It is the element type of
-// Call/CallExport arguments and results and of host-import args/results.
+// Value is a component-level call value matching the Canonical ABI lifting of
+// a WIT type.
type Value = abi.Value
-// TypeDesc, PrimitiveDesc, and the rest of the WIT type vocabulary live in
-// types.go.
-
-// HostFunc implements a synchronous component import: it receives the lifted
-// arguments and returns the lifted results (or an error, which traps the guest
-// call). Register it with WithImport.
+// HostFunc implements a synchronous component import.
type HostFunc = instance.HostFunc
-// AsyncHostFunc implements an async-lowered component import. It receives the
-// lifted arguments and an *AsyncCall used to deliver the result -- synchronously
-// (call.Resolve before returning) or later, from any goroutine, once the
-// call was started via Instance.CallAsync. Register it with WithAsyncImport.
+// AsyncHostFunc implements an async-lowered component import.
type AsyncHostFunc = instance.AsyncHostFunc
-// AsyncCall is the completion handle an AsyncHostFunc receives. Call Resolve
-// with the import's results (or ResolveCancelled). Under CallAsync, Resolve may
-// be called from another goroutine after the AsyncHostFunc returns -- that is
-// how external I/O completions drive a component forward.
+// AsyncCall is the completion handle supplied to an AsyncHostFunc.
type AsyncCall = instance.AsyncCall
-// WithImport registers fn as the component's synchronous import iface/name, with
-// the given WIT param/result types. iface is the interface name (e.g.
-// "wasi:cli/environment") or "" for a top-level import; name is the function
-// (or "" for a bare top-level func import).
+// WithImport registers a synchronous component import.
func WithImport(iface, name string, fn HostFunc, params, results []TypeDesc) Option {
return instance.WithImport(iface, name, fn, params, results)
}
-// WithAsyncImport registers fn as the component's async-lowered import
-// iface/name. Pair it with Instance.CallAsync so fn may complete the call later,
-// from another goroutine (real I/O), via AsyncCall.Resolve.
+// WithAsyncImport registers an async-lowered component import.
func WithAsyncImport(iface, name string, fn AsyncHostFunc, params, results []TypeDesc) Option {
return instance.WithAsyncImport(iface, name, fn, params, results)
}
diff --git a/component_test.go b/component_test.go
index 725caf0..4f1cd39 100644
--- a/component_test.go
+++ b/component_test.go
@@ -3,12 +3,18 @@ package component_test
import (
"context"
_ "embed"
+ "encoding/json"
"errors"
+ "os"
+ "reflect"
+ "sort"
"testing"
+ "time"
- "github.com/wago-org/component-model"
+ component "github.com/wago-org/component-model"
+ componentregister "github.com/wago-org/component-model/register"
"github.com/wago-org/wago"
- "github.com/wago-org/wago/plugin"
+ wagoplugin "github.com/wago-org/wago/plugin"
)
// This fixture is a genuine Component Model binary, not a core Wasm module.
@@ -17,114 +23,330 @@ import (
//go:embed testdata/adder.wasm
var adderWasm []byte
-type componentServiceConsumer struct {
- ref *plugin.Ref[component.Service]
-}
+type registerFunc func(*wago.Registrar) error
+
+func (f registerFunc) Register(reg *wago.Registrar) error { return f(reg) }
-func (*componentServiceConsumer) Info() wago.ExtensionInfo {
- return wago.ExtensionInfo{ID: "test.component-consumer"}
+func testDefinition(id string) wago.PluginDefinition {
+ return wago.PluginDefinition{
+ ID: id,
+ Version: "1.0.0",
+ Provenance: wago.PluginProvenance{
+ Repository: "https://" + id,
+ License: "MIT",
+ },
+ }
}
-func (e *componentServiceConsumer) Register(reg *wago.Registry) (err error) {
- e.ref, err = plugin.Require(reg, component.RuntimeService)
- return err
+
+func consumerProvider(ref **wagoplugin.Ref[component.Service]) wago.PluginProvider {
+ definition := testDefinition("example.com/component-consumer")
+ definition.Requires = []wago.PluginRequirement{{ID: component.PluginID, Version: "^0.1.0"}}
+ definition.Consumes = []wago.ContractRequirement{{
+ ID: component.Contract.ID(), Major: component.Contract.Major(), Mode: wago.ContractRequired,
+ }}
+ return wago.PluginProvider{
+ Definition: definition,
+ New: func() wago.Plugin {
+ return registerFunc(func(reg *wago.Registrar) error {
+ var err error
+ *ref, err = wagoplugin.Require(reg, component.Contract)
+ return err
+ })
+ },
+ }
}
-func TestInstantiateAdder(t *testing.T) {
- ctx := context.Background()
- r := wago.NewRuntime()
- defer r.Close()
- components, err := component.Enable(r)
- if err != nil {
- t.Fatalf("Enable: %v", err)
+func pluginSet(t *testing.T, providers []wago.PluginProvider, config json.RawMessage) wago.PluginSet {
+ t.Helper()
+ set := wago.PluginSet{Providers: providers}
+ for _, provider := range providers {
+ digest, err := wago.DefinitionDigest(provider.Definition)
+ if err != nil {
+ t.Fatal(err)
+ }
+ selection := wago.PluginSelection{
+ ID: provider.Definition.ID, DefinitionDigest: digest, Direct: true,
+ Dependencies: map[string]string{},
+ }
+ for _, requirement := range provider.Definition.Requires {
+ selection.Dependencies[requirement.ID] = requirement.Version
+ }
+ if provider.Definition.ID == component.PluginID {
+ selection.Config = append(json.RawMessage(nil), config...)
+ for _, request := range provider.Definition.Authorities {
+ selection.Grants = append(selection.Grants, wago.AuthorityGrant{Name: request.Name, Scope: request.Scope})
+ }
+ }
+ for _, requirement := range provider.Definition.Consumes {
+ var owners []string
+ for _, candidate := range providers {
+ for _, provided := range candidate.Definition.Provides {
+ if provided.ID == requirement.ID && provided.Major == requirement.Major {
+ owners = append(owners, candidate.Definition.ID)
+ }
+ }
+ }
+ sort.Strings(owners)
+ selection.Contracts = append(selection.Contracts, wago.ContractBinding{
+ ID: requirement.ID, Major: requirement.Major, Providers: owners,
+ })
+ }
+ set.Selections = append(set.Selections, selection)
}
+ return set
+}
- inst, err := components.Instantiate(ctx, adderWasm)
- if err != nil {
- t.Fatalf("Instantiate: %v", err)
+func loadService(t *testing.T, config json.RawMessage) (*wago.Runtime, *wagoplugin.Ref[component.Service]) {
+ t.Helper()
+ var ref *wagoplugin.Ref[component.Service]
+ providers := []wago.PluginProvider{component.Provider(), consumerProvider(&ref)}
+ rt := wago.NewRuntime()
+ if err := rt.LoadPlugins(context.Background(), pluginSet(t, providers, config)); err != nil {
+ _ = rt.Close()
+ t.Fatal(err)
}
- defer inst.Close(ctx)
+ return rt, ref
+}
+
+func TestPluginExecutesComponentInsideContractLease(t *testing.T) {
+ rt, ref := loadService(t, nil)
+ defer rt.Close()
- got, err := inst.CallExport(ctx, "component:adder/calc", "add", uint32(2), uint32(3))
+ var escaped *component.Instance
+ err := ref.With(func(service component.Service) error {
+ return service.WithInstance(context.Background(), adderWasm, func(in *component.Instance) error {
+ escaped = in
+ got, err := in.CallExport(context.Background(), "component:adder/calc", "add", uint32(2), uint32(3))
+ if err != nil {
+ return err
+ }
+ if len(got) != 1 || got[0] != uint32(5) {
+ t.Fatalf("add(2, 3) = %#v, want [5]", got)
+ }
+ return nil
+ })
+ })
if err != nil {
- t.Fatalf("CallExport add: %v", err)
+ t.Fatal(err)
}
- if len(got) != 1 || got[0] != uint32(5) {
- t.Fatalf("add(2, 3) = %#v, want [5]", got)
+ if _, err := escaped.CallExport(context.Background(), "component:adder/calc", "add", uint32(1), uint32(1)); err == nil {
+ t.Fatal("component instance escaped its WithInstance callback")
}
}
-func TestCompileCache(t *testing.T) {
- ctx := context.Background()
- r := wago.NewRuntime()
- defer r.Close()
- components, err := component.Enable(r)
- if err != nil {
- t.Fatalf("Enable: %v", err)
+func TestPluginOperatesWithinNarrowedInstantiationGrant(t *testing.T) {
+ var ref *wagoplugin.Ref[component.Service]
+ providers := []wago.PluginProvider{component.Provider(), consumerProvider(&ref)}
+ set := pluginSet(t, providers, nil)
+ for i := range set.Selections {
+ if set.Selections[i].ID != component.PluginID {
+ continue
+ }
+ for j := range set.Selections[i].Grants {
+ if set.Selections[i].Grants[j].Name == wago.AuthorityCoreInstanceInstantiate {
+ set.Selections[i].Grants[j].Scope = wago.AuthorityScope{MaxInstances: 1, MaxMemoryBytes: 4 << 30}
+ }
+ }
+ }
+ rt := wago.NewRuntime()
+ defer rt.Close()
+ if err := rt.LoadPlugins(context.Background(), set); err != nil {
+ t.Fatal(err)
+ }
+ if err := ref.With(func(service component.Service) error {
+ return service.WithInstance(context.Background(), adderWasm, func(in *component.Instance) error {
+ _, err := in.CallExport(context.Background(), "component:adder/calc", "add", uint32(1), uint32(2))
+ return err
+ })
+ }); err != nil {
+ t.Fatal(err)
}
+}
+
+func TestPluginCompileCache(t *testing.T) {
+ rt, ref := loadService(t, nil)
cache := component.NewCompileCache()
- defer cache.Close(ctx)
+ defer rt.Close()
+ defer cache.Close(context.Background())
for i := 0; i < 2; i++ {
- inst, err := components.Instantiate(ctx, adderWasm, component.WithCompileCache(cache))
- if err != nil {
- t.Fatalf("Instantiate #%d: %v", i, err)
- }
- got, err := inst.CallExport(ctx, "component:adder/calc", "add", uint32(10), uint32(20))
+ err := ref.With(func(service component.Service) error {
+ return service.WithInstance(context.Background(), adderWasm, func(in *component.Instance) error {
+ got, err := in.CallExport(context.Background(), "component:adder/calc", "add", uint32(10), uint32(20))
+ if err != nil {
+ return err
+ }
+ if len(got) != 1 || got[0] != uint32(30) {
+ t.Fatalf("call #%d = %#v, want [30]", i, got)
+ }
+ return nil
+ }, component.WithCompileCache(cache))
+ })
if err != nil {
- t.Fatalf("Call #%d: %v", i, err)
- }
- if len(got) != 1 || got[0] != uint32(30) {
- t.Fatalf("add(10, 20) = %#v, want [30]", got)
+ t.Fatalf("call #%d: %v", i, err)
}
- if err := inst.Close(ctx); err != nil {
- t.Fatalf("Close #%d: %v", i, err)
+ }
+}
+
+func TestPluginShutdownWaitsForComponentCallbackAndRevokesContract(t *testing.T) {
+ rt, ref := loadService(t, nil)
+ entered := make(chan struct{})
+ release := make(chan struct{})
+ callDone := make(chan error, 1)
+ go func() {
+ callDone <- ref.With(func(service component.Service) error {
+ return service.WithInstance(context.Background(), adderWasm, func(*component.Instance) error {
+ close(entered)
+ <-release
+ return nil
+ })
+ })
+ }()
+ <-entered
+ closeDone := make(chan error, 1)
+ go func() { closeDone <- rt.Close() }()
+ select {
+ case err := <-closeDone:
+ t.Fatalf("runtime closed before callback returned: %v", err)
+ case <-time.After(50 * time.Millisecond):
+ }
+ close(release)
+ if err := <-callDone; err != nil {
+ t.Fatal(err)
+ }
+ if err := <-closeDone; err != nil {
+ t.Fatal(err)
+ }
+ if err := ref.With(func(component.Service) error { return nil }); !errors.Is(err, wago.ErrPermissionDenied) {
+ t.Fatalf("contract after close = %v, want permission denied", err)
+ }
+}
+
+func TestPluginRejectsMissingAuthorityAndStrictConfig(t *testing.T) {
+ providers := []wago.PluginProvider{component.Provider()}
+ set := pluginSet(t, providers, nil)
+ set.Selections[0].Grants = set.Selections[0].Grants[:2]
+ rt := wago.NewRuntime()
+ defer rt.Close()
+ if err := rt.LoadPlugins(context.Background(), set); !errors.Is(err, wago.ErrPermissionDenied) {
+ t.Fatalf("missing authority = %v, want permission denied", err)
+ }
+
+ for _, config := range []json.RawMessage{
+ json.RawMessage(`{"unknown":true}`),
+ json.RawMessage(`null`),
+ json.RawMessage(`{} {}`),
+ } {
+ set := pluginSet(t, providers, config)
+ if err := wago.ValidatePluginSet(set); err == nil {
+ t.Fatalf("accepted invalid config %s", config)
}
}
}
-func TestPluginFailsClosedWithoutCapabilityOrInstallation(t *testing.T) {
- ctx := context.Background()
- r := wago.NewRuntime()
- defer r.Close()
+func TestConsumerGraphRequiresPackageAndExactContractBinding(t *testing.T) {
+ var ref *wagoplugin.Ref[component.Service]
+ consumer := consumerProvider(&ref)
+ if err := wago.ValidatePluginSet(pluginSet(t, []wago.PluginProvider{consumer}, nil)); err == nil {
+ t.Fatal("accepted component consumer without its package and contract provider")
+ }
- if _, err := component.Instantiate(ctx, r, adderWasm); err == nil {
- t.Fatal("Instantiate without the component plugin succeeded")
+ providers := []wago.PluginProvider{component.Provider(), consumer}
+ set := pluginSet(t, providers, nil)
+ for i := range set.Selections {
+ if set.Selections[i].ID == consumer.Definition.ID {
+ set.Selections[i].Contracts = nil
+ }
+ }
+ if err := wago.ValidatePluginSet(set); err == nil {
+ t.Fatal("accepted component consumer without its reviewed contract binding")
}
- if err := r.Use(component.NewExtension(), wago.WithPluginGrants()); !errors.Is(err, wago.ErrPermissionDenied) {
- t.Fatalf("Use without core.runtime grant = %v, want permission denied", err)
+
+ incompatible := consumerProvider(&ref)
+ incompatible.Definition.Requires[0].Version = ">=1.0.0"
+ if err := wago.ValidatePluginSet(pluginSet(t, []wago.PluginProvider{component.Provider(), incompatible}, nil)); err == nil {
+ t.Fatal("accepted component provider outside the consumer's version range")
}
}
-func TestPluginRuntimeAccessIsRevokedOnClose(t *testing.T) {
- r := wago.NewRuntime()
- components, err := component.Enable(r)
- if err != nil {
- t.Fatalf("Enable: %v", err)
+func TestDefinitionUsesExactAuthoritiesAndVersionedContract(t *testing.T) {
+ definition := component.Definition()
+ if definition.ID != "github.com/wago-org/component-model" {
+ t.Fatalf("plugin ID = %q", definition.ID)
}
- if err := r.Close(); err != nil {
- t.Fatalf("Close: %v", err)
+ if got, want := definition.Provides, []wago.ContractSpec{component.Contract.Spec()}; len(got) != len(want) || got[0] != want[0] {
+ t.Fatalf("provided contracts = %#v, want %#v", got, want)
}
- if _, err := components.Instantiate(context.Background(), adderWasm); !errors.Is(err, wago.ErrPermissionDenied) {
- t.Fatalf("Instantiate after Close = %v, want permission denied", err)
+ wantAuthorities := []wago.Authority{
+ wago.AuthorityCoreModuleCompile,
+ wago.AuthorityCoreInstanceInstantiate,
+ wago.AuthorityCoreFuncRefCreate,
+ }
+ if len(definition.Authorities) != len(wantAuthorities) {
+ t.Fatalf("authorities = %#v", definition.Authorities)
+ }
+ for i, want := range wantAuthorities {
+ if got := definition.Authorities[i]; got.Name != want || got.Mode != wago.AuthorityRequired {
+ t.Fatalf("authority[%d] = %#v, want required %q", i, got, want)
+ }
+ }
+ scope := definition.Authorities[1].Scope
+ if scope.MaxInstances != 64 || scope.MaxMemoryBytes != 16<<30 {
+ t.Fatalf("core instantiation scope = %#v, want 64 instances and 16 GiB", scope)
}
}
-func TestPluginProvidesVersionedRuntimeService(t *testing.T) {
- if component.PluginID != "wago-org/component-model" {
- t.Fatalf("PluginID = %q", component.PluginID)
+func TestManifestMetadataMatchesProviderDefinition(t *testing.T) {
+ raw, err := os.ReadFile("wago.json")
+ if err != nil {
+ t.Fatal(err)
+ }
+ var manifest struct {
+ Schema string `json:"$schema"`
+ Package struct {
+ Module string `json:"module"`
+ Version string `json:"version"`
+ Name string `json:"name"`
+ Description string `json:"description"`
+ Stability wago.Stability `json:"stability"`
+ License string `json:"license"`
+ Repository string `json:"repository"`
+ Homepage string `json:"homepage"`
+ Engines map[string]string `json:"engines"`
+ Authors []struct {
+ Name string `json:"name"`
+ } `json:"authors"`
+ } `json:"package"`
+ }
+ if err := json.Unmarshal(raw, &manifest); err != nil {
+ t.Fatal(err)
+ }
+ if manifest.Schema != "https://wago.sh/v1/schema.json" {
+ t.Fatalf("manifest schema = %q", manifest.Schema)
+ }
+ definition := component.Definition()
+ pkg := manifest.Package
+ if pkg.Module != definition.ID || pkg.Version != definition.Version || pkg.Name != definition.Name ||
+ pkg.Description != definition.Description || pkg.Stability != definition.Stability ||
+ pkg.License != definition.Provenance.License || pkg.Repository != definition.Provenance.Repository ||
+ pkg.Homepage != definition.Provenance.Homepage || len(pkg.Authors) != len(definition.Provenance.Authors) ||
+ len(pkg.Authors) != 1 || pkg.Authors[0].Name != definition.Provenance.Authors[0] ||
+ pkg.Engines["wago"] != definition.Compatibility.Engines["wago"] {
+ t.Fatalf("manifest package metadata does not match provider definition: package=%#v definition=%#v", pkg, definition)
+ }
+ providers := componentregister.Providers()
+ want, err := wago.EncodeProviderCatalog("github.com/wago-org/component-model/register", providers)
+ if err != nil {
+ t.Fatal(err)
}
- r := wago.NewRuntime()
- defer r.Close()
- components, err := component.Enable(r)
+ got, err := os.ReadFile(wago.ProviderCatalogFile)
if err != nil {
- t.Fatalf("Enable: %v", err)
+ t.Fatal(err)
}
- consumer := &componentServiceConsumer{}
- if err := r.Use(consumer); err != nil {
- t.Fatalf("Use consumer: %v", err)
+ if !reflect.DeepEqual(got, want) {
+ t.Fatalf("%s is stale; run wago plugin catalog", wago.ProviderCatalogFile)
}
- service, err := consumer.ref.Get()
- if err != nil || service != components {
- t.Fatalf("component service = %#v, %v; want %#v", service, err, components)
+ if _, err := wago.DecodeProviderCatalog(got); err != nil {
+ t.Fatalf("%s: %v", wago.ProviderCatalogFile, err)
}
}
diff --git a/go.mod b/go.mod
index 1a112ee..dd0ce9c 100644
--- a/go.mod
+++ b/go.mod
@@ -2,4 +2,4 @@ module github.com/wago-org/component-model
go 1.22.0
-require github.com/wago-org/wago v0.0.0-20260811034746-b4839b10d386
+require github.com/wago-org/wago v0.0.0-20260812144524-1c58c9862d25
diff --git a/go.sum b/go.sum
index a70e609..24b3655 100644
--- a/go.sum
+++ b/go.sum
@@ -1,2 +1,2 @@
-github.com/wago-org/wago v0.0.0-20260811034746-b4839b10d386 h1:GZSwU7NMpl/qks6XpkRKgqB7vH+R7c5b4YJL3F8VLrE=
-github.com/wago-org/wago v0.0.0-20260811034746-b4839b10d386/go.mod h1:6XmxI3S5qJ+YAzObyXjC6FmxHtbLhBBxrglzix42zl8=
+github.com/wago-org/wago v0.0.0-20260812144524-1c58c9862d25 h1:gopNHvaUaDSzmW2Hfd32r8dOwwzYUHH3ph1L/IuhJUA=
+github.com/wago-org/wago v0.0.0-20260812144524-1c58c9862d25/go.mod h1:6XmxI3S5qJ+YAzObyXjC6FmxHtbLhBBxrglzix42zl8=
diff --git a/internal/engine/engine.go b/internal/engine/engine.go
index a9c3fab..0048602 100644
--- a/internal/engine/engine.go
+++ b/internal/engine/engine.go
@@ -117,6 +117,13 @@ func (c ModuleConfig) WithStartFunctions(...string) ModuleConfig { return c }
type CompiledModule interface{ Close(context.Context) error }
+func cleanupContext(ctx context.Context) context.Context {
+ if ctx == nil {
+ return context.Background()
+ }
+ return context.WithoutCancel(ctx)
+}
+
type Runtime interface {
InstantiateWithConfig(context.Context, []byte, ModuleConfig) (Module, error)
CompileModule(context.Context, []byte) (CompiledModule, error)
@@ -130,24 +137,37 @@ type CoreFuncImport struct {
Params, Results []ValueType
}
-type runtimeAdapter struct{ rt core.CoreRuntime }
+type runtimeAdapter struct {
+ compiler *core.CoreModuleCompiler
+ instantiator *core.CoreInstanceInstantiator
+ funcrefs *core.CoreFuncRefFactory
+}
-func Wrap(rt core.CoreRuntime) Runtime { return &runtimeAdapter{rt: rt} }
+func Wrap(compiler *core.CoreModuleCompiler, instantiator *core.CoreInstanceInstantiator, funcrefs *core.CoreFuncRefFactory) Runtime {
+ return &runtimeAdapter{compiler: compiler, instantiator: instantiator, funcrefs: funcrefs}
+}
type compiledModule struct{ mod *core.Module }
-func (c *compiledModule) Close(context.Context) error { return c.mod.Close() }
+func (c *compiledModule) Close(context.Context) error {
+ if c == nil || c.mod == nil || c.mod.Compiled() == nil {
+ return nil
+ }
+ err := c.mod.Compiled().Close()
+ c.mod = nil
+ return err
+}
func (r *runtimeAdapter) CompileModule(ctx context.Context, source []byte) (CompiledModule, error) {
- if r == nil || r.rt == nil {
- return nil, fmt.Errorf("component: nil Wago runtime")
- }
if ctx != nil {
if err := ctx.Err(); err != nil {
return nil, err
}
}
- m, err := r.rt.Compile(append([]byte(nil), source...))
+ if r == nil || r.compiler == nil {
+ return nil, fmt.Errorf("component: nil core module compiler")
+ }
+ m, err := r.compiler.Compile(append([]byte(nil), source...))
if err != nil {
return nil, err
}
@@ -159,6 +179,7 @@ func (r *runtimeAdapter) InspectModuleImports(ctx context.Context, source []byte
if err != nil {
return nil, err
}
+ defer c.Close(cleanupContext(ctx))
cm := c.(*compiledModule)
imports := cm.mod.Imports()
out := make([]CoreFuncImport, 0, len(imports))
@@ -176,14 +197,25 @@ func (r *runtimeAdapter) InstantiateWithConfig(ctx context.Context, source []byt
if err != nil {
return nil, err
}
- return r.InstantiateModule(ctx, c, cfg)
+ mod, err := r.instantiateModule(ctx, c, cfg, true)
+ if err != nil {
+ _ = c.Close(cleanupContext(ctx))
+ }
+ return mod, err
}
func (r *runtimeAdapter) InstantiateModule(ctx context.Context, c CompiledModule, cfg ModuleConfig) (Module, error) {
+ return r.instantiateModule(ctx, c, cfg, false)
+}
+
+func (r *runtimeAdapter) instantiateModule(ctx context.Context, c CompiledModule, cfg ModuleConfig, closeCompiled bool) (Module, error) {
cm, ok := c.(*compiledModule)
if !ok || cm == nil || cm.mod == nil {
return nil, fmt.Errorf("component: compiled module belongs to another runtime")
}
+ if r == nil || r.instantiator == nil || r.funcrefs == nil {
+ return nil, fmt.Errorf("component: incomplete core execution handles")
+ }
imports := core.Imports{}
resolvedFuncs := map[string]Function{}
hostRefs := make([]*core.HostFuncRef, 0)
@@ -227,7 +259,7 @@ func (r *runtimeAdapter) InstantiateModule(ctx context.Context, c CompiledModule
})
}
if host != nil {
- owner, err := r.rt.NewHostFuncRef(host, core.FuncSig{
+ owner, err := r.funcrefs.New(host, core.FuncSig{
Params: append([]core.ValType(nil), spec.Params...),
Results: append([]core.ValType(nil), spec.Results...),
})
@@ -255,24 +287,36 @@ func (r *runtimeAdapter) InstantiateModule(ctx context.Context, c CompiledModule
}
}
}
- in, err := r.rt.Instantiate(ctx, cm.mod, core.WithImports(imports), core.WithSynchronousHostCalls())
+ owned, err := r.instantiator.Instantiate(ctx, cm.mod, core.WithImports(imports), core.WithSynchronousHostCalls())
if err != nil {
closeHostRefs()
return nil, err
}
- return newModule(cfg.name, in, cm.mod, resolvedFuncs, hostRefs), nil
+ in := owned.Instance()
+ if in == nil {
+ _ = owned.Close()
+ closeHostRefs()
+ return nil, fmt.Errorf("component: core instantiator returned a closed instance")
+ }
+ var compiledOwner *compiledModule
+ if closeCompiled {
+ compiledOwner = cm
+ }
+ return newModule(cfg.name, owned, in, cm.mod, compiledOwner, resolvedFuncs, hostRefs), nil
}
type module struct {
name string
+ owned *core.ManagedInstance
in *core.Instance
+ compiled *compiledModule
defs map[string]FunctionDefinition
forwarded map[string]Function
hostRefs []*core.HostFuncRef
}
-func newModule(name string, in *core.Instance, compiled *core.Module, resolved map[string]Function, hostRefs []*core.HostFuncRef) *module {
- m := &module{name: name, in: in, defs: map[string]FunctionDefinition{}, forwarded: map[string]Function{}, hostRefs: hostRefs}
+func newModule(name string, owned *core.ManagedInstance, in *core.Instance, compiled *core.Module, compiledOwner *compiledModule, resolved map[string]Function, hostRefs []*core.HostFuncRef) *module {
+ m := &module{name: name, owned: owned, in: in, compiled: compiledOwner, defs: map[string]FunctionDefinition{}, forwarded: map[string]Function{}, hostRefs: hostRefs}
for _, f := range compiled.Metadata().Functions {
for _, export := range f.Exports {
m.defs[export] = functionDefinition{params: valTypes(f.Params), results: valTypes(f.Results)}
@@ -344,12 +388,17 @@ func (m *module) ExportedGlobal(name string) Global {
}
return &global{g: g}
}
-func (m *module) Close(context.Context) error {
- err := m.in.Close()
+func (m *module) Close(ctx context.Context) error {
+ err := m.owned.Close()
+ m.owned = nil
for i := len(m.hostRefs) - 1; i >= 0; i-- {
err = errors.Join(err, m.hostRefs[i].Close())
}
m.hostRefs = nil
+ if m.compiled != nil {
+ err = errors.Join(err, m.compiled.Close(cleanupContext(ctx)))
+ m.compiled = nil
+ }
return err
}
@@ -438,7 +487,6 @@ func (g *global) Set(v uint64) { _ = g.g.Set(v) }
var _ = binary.LittleEndian
type HostModuleBuilder struct {
- rt *runtimeAdapter
name string
funcs map[string]*hostFunction
pending *hostFunction
@@ -446,7 +494,7 @@ type HostModuleBuilder struct {
type HostFunctionBuilder struct{ parent HostModuleBuilder }
func (r *runtimeAdapter) NewHostModuleBuilder(name string) HostModuleBuilder {
- return HostModuleBuilder{rt: r, name: name, funcs: map[string]*hostFunction{}}
+ return HostModuleBuilder{name: name, funcs: map[string]*hostFunction{}}
}
func (b HostModuleBuilder) NewFunctionBuilder() HostFunctionBuilder {
return HostFunctionBuilder{parent: b}
diff --git a/plugin.go b/plugin.go
index 8da6e6a..694c402 100644
--- a/plugin.go
+++ b/plugin.go
@@ -1,109 +1,162 @@
package component
import (
+ "bytes"
"context"
+ "encoding/json"
+ "errors"
"fmt"
+ "io"
"github.com/wago-org/component-model/internal/engine"
"github.com/wago-org/component-model/internal/instance"
"github.com/wago-org/wago"
- "github.com/wago-org/wago/plugin"
+ wagoplugin "github.com/wago-org/wago/plugin"
)
-// PluginID is the stable extension ID for the Component Model runtime.
-const PluginID = "wago-org/component-model"
+// PluginID is the canonical Component Model plugin ID.
+const PluginID = "github.com/wago-org/component-model"
-// ServiceName is the versioned Component Model runtime service consumed by
-// plugins that provide component-level worlds such as WASI Preview 2.
-const ServiceName = "wago-org/component-model/runtime/v1"
+const (
+ // Wago charges an unbounded memory32 declaration at its finite 65,535-page
+ // implementation reservation. Sixteen GiB therefore admits four ordinary
+ // unbounded-memory modules while the separate slot limit leaves room for
+ // memoryless adapters and linker shims.
+ requestedMaxCoreInstances = 64
+ requestedMaxCoreMemoryBytes = 16 << 30
+)
+
+// Contract is the major-versioned Component Model execution service consumed
+// by WASI and other component-world plugins.
+var Contract = wagoplugin.NewContract[Service](PluginID+"/runtime", 1)
-// Service is the public execution surface provided by the Component Model
-// plugin. Depending plugins should require RuntimeService rather than importing
-// engine internals or asking for core-runtime authority themselves.
+// Service is the Component Model plugin's cross-plugin execution boundary.
+// WithInstance keeps the service and every core resource it creates inside the
+// caller's contract lease. The instance is closed before WithInstance returns
+// and must not be retained by fn.
type Service interface {
- Instantiate(context.Context, []byte, ...Option) (*Instance, error)
+ WithInstance(context.Context, []byte, func(*Instance) error, ...Option) error
}
-// RuntimeService identifies the versioned Component Model execution service.
-var RuntimeService = plugin.NewServiceKey[Service](ServiceName)
+var configSchema = json.RawMessage(`{
+ "type": "object",
+ "additionalProperties": false,
+ "maxProperties": 0
+}`)
-func init() {
- wago.RegisterExtension(PluginID, func() wago.Extension { return NewExtension() })
+// Definition returns fresh immutable metadata for the explicit provider.
+func Definition() wago.PluginDefinition {
+ return wago.PluginDefinition{
+ ID: PluginID,
+ Name: "Wago Component Model",
+ Version: "0.1.0",
+ Description: "WebAssembly Component Model execution and Canonical ABI linking for Wago.",
+ Stability: wago.Experimental,
+ Compatibility: wago.Compatibility{
+ Engines: map[string]string{"wago": ">=0.1.0"},
+ },
+ Provenance: wago.PluginProvenance{
+ Homepage: "https://github.com/wago-org/component-model#readme",
+ Repository: "https://github.com/wago-org/component-model",
+ License: "Apache-2.0",
+ Authors: []string{"Jairus Tanaka"},
+ },
+ Authorities: []wago.AuthorityRequest{
+ {
+ Name: wago.AuthorityCoreModuleCompile,
+ Mode: wago.AuthorityRequired,
+ Reason: "compile the core WebAssembly modules embedded in a component",
+ },
+ {
+ Name: wago.AuthorityCoreInstanceInstantiate,
+ Mode: wago.AuthorityRequired,
+ Reason: "instantiate and own the bounded core-module graph behind a component instance",
+ Scope: wago.AuthorityScope{
+ MaxInstances: requestedMaxCoreInstances,
+ MaxMemoryBytes: requestedMaxCoreMemoryBytes,
+ },
+ },
+ {
+ Name: wago.AuthorityCoreFuncRefCreate,
+ Mode: wago.AuthorityRequired,
+ Reason: "bridge Canonical ABI lifts and lowers through typed host function references",
+ },
+ },
+ ConfigSchema: append(json.RawMessage(nil), configSchema...),
+ Provides: []wago.ContractSpec{Contract.Spec()},
+ }
}
-// Extension installs Component Model execution into a Wago runtime. Use Enable
-// for programmatic installation, or register NewExtension in a manifest-driven
-// host with the core.runtime plugin capability granted.
-type Extension struct {
- runtime *Runtime
+// Provider is the side-effect-free catalog entry for Component Model support.
+func Provider() wago.PluginProvider {
+ return wago.PluginProvider{
+ Definition: Definition(),
+ New: func() wago.Plugin { return new(componentPlugin) },
+ ValidateConfig: validateConfig,
+ }
}
-// NewExtension returns an unregistered Component Model extension.
-func NewExtension() *Extension { return &Extension{} }
-
-func (*Extension) Info() wago.ExtensionInfo {
- return wago.ExtensionInfo{
- ID: PluginID,
- Name: "WebAssembly Component Model",
- Description: "Decodes, links, and executes WebAssembly Components",
- Stability: wago.Experimental,
- Repository: "https://github.com/wago-org/component-model",
- License: "Apache-2.0",
- Tags: []string{"component-model", "canonical-abi"},
- RequiresCapabilities: []wago.PluginCapability{wago.PluginCoreRuntime},
+func validateConfig(raw json.RawMessage) error {
+ if len(raw) == 0 {
+ raw = json.RawMessage(`{}`)
+ }
+ dec := json.NewDecoder(bytes.NewReader(raw))
+ dec.DisallowUnknownFields()
+ var cfg struct{}
+ if err := dec.Decode(&cfg); err != nil {
+ return fmt.Errorf("component: config: %w", err)
+ }
+ if bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
+ return fmt.Errorf("component: config must be an object")
}
+ if err := dec.Decode(new(any)); !errors.Is(err, io.EOF) {
+ return fmt.Errorf("component: config has a trailing JSON value")
+ }
+ return nil
}
-func (e *Extension) Register(reg *wago.Registry) error {
- access, err := reg.CoreRuntime()
+type componentPlugin struct{}
+
+func (*componentPlugin) Register(reg *wago.Registrar) error {
+ var cfg struct{}
+ if err := reg.Config(&cfg); err != nil {
+ return err
+ }
+ compiler, err := reg.CoreModuleCompiler()
if err != nil {
return err
}
- e.runtime = &Runtime{engine: access}
- return plugin.Provide[Service](reg, RuntimeService, e.runtime)
-}
-
-// Runtime is the capability-scoped Component Model execution service installed
-// in one Wago runtime. It cannot be moved to or used with another runtime.
-type Runtime struct {
- engine wago.CoreRuntime
-}
-
-// Enable installs the Component Model plugin with its required authority and
-// returns the runtime-scoped component service.
-func Enable(rt *wago.Runtime) (*Runtime, error) {
- if rt == nil {
- return nil, fmt.Errorf("component: enable on nil Wago runtime")
+ instantiator, err := reg.CoreInstanceInstantiator()
+ if err != nil {
+ return err
}
- if err := rt.UsePlugin(PluginID, wago.WithPluginGrants(wago.PluginCoreRuntime)); err != nil {
- return nil, err
+ funcrefs, err := reg.CoreFuncRefFactory()
+ if err != nil {
+ return err
}
- return FromRuntime(rt)
+ service := &runtimeService{engine: engine.Wrap(compiler, instantiator, funcrefs)}
+ return wagoplugin.Provide(reg, Contract, Service(service))
}
-// FromRuntime resolves the installed Component Model plugin. It fails closed
-// when the plugin is absent, has the wrong concrete implementation, or its
-// runtime authority has been revoked.
-func FromRuntime(rt *wago.Runtime) (*Runtime, error) {
- if rt == nil {
- return nil, fmt.Errorf("component: nil Wago runtime")
- }
- ext, ok := rt.Extension(PluginID)
- if !ok {
- return nil, fmt.Errorf("component: plugin %q is not enabled", PluginID)
- }
- plugin, ok := ext.(*Extension)
- if !ok || plugin == nil || plugin.runtime == nil || plugin.runtime.engine == nil {
- return nil, fmt.Errorf("component: plugin %q has an invalid implementation", PluginID)
- }
- return plugin.runtime, nil
+type runtimeService struct {
+ engine engine.Runtime
}
-// Instantiate decodes and instantiates a component through this plugin's
-// authorized core-runtime handle.
-func (r *Runtime) Instantiate(ctx context.Context, componentBytes []byte, opts ...Option) (*Instance, error) {
+func (r *runtimeService) WithInstance(ctx context.Context, componentBytes []byte, fn func(*Instance) error, opts ...Option) (err error) {
if r == nil || r.engine == nil {
- return nil, fmt.Errorf("component: nil or inactive component runtime")
+ return fmt.Errorf("component: inactive component service")
+ }
+ if ctx == nil {
+ return fmt.Errorf("component: nil context")
+ }
+ if fn == nil {
+ return fmt.Errorf("component: nil instance callback")
+ }
+ in, err := instance.Instantiate(ctx, r.engine, componentBytes, opts...)
+ if err != nil {
+ return err
}
- return instance.Instantiate(ctx, engine.Wrap(r.engine), componentBytes, opts...)
+ closeCtx := context.WithoutCancel(ctx)
+ defer func() { err = errors.Join(err, in.Close(closeCtx)) }()
+ return fn(in)
}
diff --git a/register/catalog.go b/register/catalog.go
new file mode 100644
index 0000000..f05e0ef
--- /dev/null
+++ b/register/catalog.go
@@ -0,0 +1,13 @@
+// Package register exposes the Component Model provider catalog to generated
+// Wago runtimes. Importing this package has no registration side effects.
+package register
+
+import (
+ component "github.com/wago-org/component-model"
+ "github.com/wago-org/wago"
+)
+
+// Providers returns fresh explicit catalog entries.
+func Providers() []wago.PluginProvider {
+ return []wago.PluginProvider{component.Provider()}
+}
diff --git a/register/catalog_test.go b/register/catalog_test.go
new file mode 100644
index 0000000..51ce116
--- /dev/null
+++ b/register/catalog_test.go
@@ -0,0 +1,25 @@
+package register
+
+import (
+ "testing"
+
+ component "github.com/wago-org/component-model"
+)
+
+func TestProvidersReturnsExplicitComponentCatalog(t *testing.T) {
+ providers := Providers()
+ if len(providers) != 1 {
+ t.Fatalf("Providers length = %d, want 1", len(providers))
+ }
+ if got := providers[0].Definition.ID; got != component.PluginID {
+ t.Fatalf("provider ID = %q, want %q", got, component.PluginID)
+ }
+ if providers[0].New == nil || providers[0].ValidateConfig == nil {
+ t.Fatal("provider is missing its factory or config validator")
+ }
+
+ providers[0].Definition.Name = "mutated"
+ if got := Providers()[0].Definition.Name; got == "mutated" {
+ t.Fatal("Providers returned shared mutable metadata")
+ }
+}
diff --git a/register/register.go b/register/register.go
deleted file mode 100644
index 0c59cab..0000000
--- a/register/register.go
+++ /dev/null
@@ -1,5 +0,0 @@
-// Package register wires the Component Model plugin into Wago's global plugin
-// registry as a side effect of import.
-package register
-
-import _ "github.com/wago-org/component-model"
diff --git a/register/register_test.go b/register/register_test.go
deleted file mode 100644
index 831f98e..0000000
--- a/register/register_test.go
+++ /dev/null
@@ -1,18 +0,0 @@
-package register
-
-import (
- "testing"
-
- component "github.com/wago-org/component-model"
- "github.com/wago-org/wago"
-)
-
-func TestComponentModelPluginIsRegistered(t *testing.T) {
- ext, ok := wago.NewExtension(component.PluginID)
- if !ok {
- t.Fatalf("plugin %q is not registered", component.PluginID)
- }
- if ext.Info().ID != component.PluginID {
- t.Fatalf("registered plugin ID = %q", ext.Info().ID)
- }
-}
diff --git a/wago.json b/wago.json
index ba3609a..55c283c 100644
--- a/wago.json
+++ b/wago.json
@@ -1,13 +1,17 @@
{
- "$schema": "https://wago.sh/v0/schema.json",
- "module": "github.com/wago-org/component-model",
- "version": "0.0.0",
- "name": "Wago Component Model",
- "short": "component-model",
- "description": "Capability-gated WebAssembly Component Model runtime for Wago plugins.",
- "license": "Apache-2.0",
- "repository": "https://github.com/wago-org/component-model",
- "homepage": "https://github.com/wago-org/component-model#readme",
- "category": "runtime",
- "tags": ["webassembly", "component-model", "canonical-abi"]
+ "$schema": "https://wago.sh/v1/schema.json",
+ "package": {
+ "module": "github.com/wago-org/component-model",
+ "version": "0.1.0",
+ "name": "Wago Component Model",
+ "description": "WebAssembly Component Model execution and Canonical ABI linking for Wago.",
+ "stability": "experimental",
+ "license": "Apache-2.0",
+ "repository": "https://github.com/wago-org/component-model",
+ "homepage": "https://github.com/wago-org/component-model#readme",
+ "category": "runtime",
+ "tags": ["webassembly", "component-model", "canonical-abi", "runtime"],
+ "authors": [{"name": "Jairus Tanaka", "github": "JairusSW"}],
+ "engines": {"wago": ">=0.1.0"}
+ }
}
diff --git a/wago.providers.json b/wago.providers.json
new file mode 100644
index 0000000..dfa86a5
--- /dev/null
+++ b/wago.providers.json
@@ -0,0 +1,63 @@
+{
+ "$schema": "https://wago.sh/v1/providers.schema.json",
+ "providers": [
+ {
+ "importPath": "github.com/wago-org/component-model/register",
+ "definition": {
+ "id": "github.com/wago-org/component-model",
+ "name": "Wago Component Model",
+ "version": "0.1.0",
+ "description": "WebAssembly Component Model execution and Canonical ABI linking for Wago.",
+ "stability": "experimental",
+ "compatibility": {
+ "engines": {
+ "wago": "\u003e=0.1.0"
+ }
+ },
+ "provenance": {
+ "homepage": "https://github.com/wago-org/component-model#readme",
+ "repository": "https://github.com/wago-org/component-model",
+ "license": "Apache-2.0",
+ "authors": [
+ "Jairus Tanaka"
+ ]
+ },
+ "authorities": [
+ {
+ "name": "core.funcref.create",
+ "mode": "required",
+ "reason": "bridge Canonical ABI lifts and lowers through typed host function references",
+ "scope": {}
+ },
+ {
+ "name": "core.instance.instantiate",
+ "mode": "required",
+ "reason": "instantiate and own the bounded core-module graph behind a component instance",
+ "scope": {
+ "maxInstances": 64,
+ "maxMemoryBytes": 17179869184
+ }
+ },
+ {
+ "name": "core.module.compile",
+ "mode": "required",
+ "reason": "compile the core WebAssembly modules embedded in a component",
+ "scope": {}
+ }
+ ],
+ "configSchema": {
+ "additionalProperties": false,
+ "maxProperties": 0,
+ "type": "object"
+ },
+ "provides": [
+ {
+ "id": "github.com/wago-org/component-model/runtime",
+ "major": 1
+ }
+ ]
+ },
+ "definitionDigest": "sha256:3f72634f84e8ffad0b7460f755b56b56c643b4e04f930579db08d41b3dd7083f"
+ }
+ ]
+}