From 2a92d67145b2df7ad0efba89f8766c9f5babbb15 Mon Sep 17 00:00:00 2001 From: JairusSW Date: Wed, 12 Aug 2026 06:44:03 -0700 Subject: [PATCH 1/7] plugins: migrate component model to vNext Why: - the ecosystem must use Wago's explicit, authority-scoped provider and contract model What: - replace legacy registration with canonical provider catalogs - declare strict configuration, authorities, lifecycle, and reviewed graph metadata - pin the exact Wago vNext revision and remove local filesystem overrides Proof: - race tests, vet, and catalog check pass Docs: - README and plugin metadata updated for vNext --- README.md | 463 ++++++++++++-------------------------- component.go | 116 +++------- component_test.go | 370 ++++++++++++++++++++++++------ go.mod | 2 +- go.sum | 4 +- internal/engine/engine.go | 82 +++++-- plugin.go | 199 ++++++++++------ register/catalog.go | 13 ++ register/catalog_test.go | 25 ++ register/register.go | 5 - register/register_test.go | 18 -- wago.json | 26 ++- wago.providers.json | 63 ++++++ 13 files changed, 779 insertions(+), 607 deletions(-) create mode 100644 register/catalog.go create mode 100644 register/catalog_test.go delete mode 100644 register/register.go delete mode 100644 register/register_test.go create mode 100644 wago.providers.json 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.

- CI - Go >= 1.22 - Wago >= 0.1.0 + CI + Go >= 1.22

-
-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..1a515b0 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-20260812133922-747a0520edb8 diff --git a/go.sum b/go.sum index a70e609..aebaed8 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-20260812133922-747a0520edb8 h1:VHkyQAIQxbcHYp4OffVuGZzUxyXYxBkgGYHTDxTJ/So= +github.com/wago-org/wago v0.0.0-20260812133922-747a0520edb8/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" + } + ] +} From 22d630f29bec6bee8d6e43964328b59617e84cb4 Mon Sep 17 00:00:00 2001 From: JairusSW Date: Wed, 12 Aug 2026 06:59:23 -0700 Subject: [PATCH 2/7] chore: pin repaired plugin runtime --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 1a515b0..bd97de2 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-20260812133922-747a0520edb8 +require github.com/wago-org/wago v0.0.0-20260812135637-485943d049a3 diff --git a/go.sum b/go.sum index aebaed8..e1cb228 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,2 @@ -github.com/wago-org/wago v0.0.0-20260812133922-747a0520edb8 h1:VHkyQAIQxbcHYp4OffVuGZzUxyXYxBkgGYHTDxTJ/So= -github.com/wago-org/wago v0.0.0-20260812133922-747a0520edb8/go.mod h1:6XmxI3S5qJ+YAzObyXjC6FmxHtbLhBBxrglzix42zl8= +github.com/wago-org/wago v0.0.0-20260812135637-485943d049a3 h1:0G6XWikBuovKJQADEhAI4P+MTiUUQIvtGhRtsgNpHkY= +github.com/wago-org/wago v0.0.0-20260812135637-485943d049a3/go.mod h1:6XmxI3S5qJ+YAzObyXjC6FmxHtbLhBBxrglzix42zl8= From 2611f20413702dc13ff9ec1c3c7a66bc5c5e4cc1 Mon Sep 17 00:00:00 2001 From: JairusSW Date: Wed, 12 Aug 2026 07:05:14 -0700 Subject: [PATCH 3/7] chore: pin final plugin runtime --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index bd97de2..ead6269 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-20260812135637-485943d049a3 +require github.com/wago-org/wago v0.0.0-20260812140322-f669c98a153e diff --git a/go.sum b/go.sum index e1cb228..771c526 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,2 @@ -github.com/wago-org/wago v0.0.0-20260812135637-485943d049a3 h1:0G6XWikBuovKJQADEhAI4P+MTiUUQIvtGhRtsgNpHkY= -github.com/wago-org/wago v0.0.0-20260812135637-485943d049a3/go.mod h1:6XmxI3S5qJ+YAzObyXjC6FmxHtbLhBBxrglzix42zl8= +github.com/wago-org/wago v0.0.0-20260812140322-f669c98a153e h1:lBgpSR9MAVZvlF3RNNpKzwTF6C4ZtyeNqu9pfKzQwYE= +github.com/wago-org/wago v0.0.0-20260812140322-f669c98a153e/go.mod h1:6XmxI3S5qJ+YAzObyXjC6FmxHtbLhBBxrglzix42zl8= From 06448cd64a401e2505829f079b8c28870840a303 Mon Sep 17 00:00:00 2001 From: JairusSW Date: Wed, 12 Aug 2026 07:11:44 -0700 Subject: [PATCH 4/7] chore: pin final tested plugin runtime --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index ead6269..43c801e 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-20260812140322-f669c98a153e +require github.com/wago-org/wago v0.0.0-20260812141033-492d7b4905ea diff --git a/go.sum b/go.sum index 771c526..a77f51d 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,2 @@ -github.com/wago-org/wago v0.0.0-20260812140322-f669c98a153e h1:lBgpSR9MAVZvlF3RNNpKzwTF6C4ZtyeNqu9pfKzQwYE= -github.com/wago-org/wago v0.0.0-20260812140322-f669c98a153e/go.mod h1:6XmxI3S5qJ+YAzObyXjC6FmxHtbLhBBxrglzix42zl8= +github.com/wago-org/wago v0.0.0-20260812141033-492d7b4905ea h1:SU/eDjjh7SFPwc9gDT7qFqqVIA+N/YND3lFqoSDcqbg= +github.com/wago-org/wago v0.0.0-20260812141033-492d7b4905ea/go.mod h1:6XmxI3S5qJ+YAzObyXjC6FmxHtbLhBBxrglzix42zl8= From 778e7a701f0b12c60d86b69a13a4a7dacf382688 Mon Sep 17 00:00:00 2001 From: JairusSW Date: Wed, 12 Aug 2026 07:26:40 -0700 Subject: [PATCH 5/7] chore: pin cross-platform plugin runtime --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 43c801e..ddd4bb3 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-20260812141033-492d7b4905ea +require github.com/wago-org/wago v0.0.0-20260812142346-bdfc6aaafa12 diff --git a/go.sum b/go.sum index a77f51d..f55228a 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,2 @@ -github.com/wago-org/wago v0.0.0-20260812141033-492d7b4905ea h1:SU/eDjjh7SFPwc9gDT7qFqqVIA+N/YND3lFqoSDcqbg= -github.com/wago-org/wago v0.0.0-20260812141033-492d7b4905ea/go.mod h1:6XmxI3S5qJ+YAzObyXjC6FmxHtbLhBBxrglzix42zl8= +github.com/wago-org/wago v0.0.0-20260812142346-bdfc6aaafa12 h1:Eugr8XolBXuXJGXbKW7vYJIWWRSnn2HepWXxo9CzMSs= +github.com/wago-org/wago v0.0.0-20260812142346-bdfc6aaafa12/go.mod h1:6XmxI3S5qJ+YAzObyXjC6FmxHtbLhBBxrglzix42zl8= From 640f6615068e6b8ace38c3a713418c4c59fd5f6d Mon Sep 17 00:00:00 2001 From: JairusSW Date: Wed, 12 Aug 2026 07:41:15 -0700 Subject: [PATCH 6/7] chore: pin final plugin runtime --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index ddd4bb3..0d37c01 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-20260812142346-bdfc6aaafa12 +require github.com/wago-org/wago v0.0.0-20260812143921-6f5623623b4f diff --git a/go.sum b/go.sum index f55228a..62409d4 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,2 @@ -github.com/wago-org/wago v0.0.0-20260812142346-bdfc6aaafa12 h1:Eugr8XolBXuXJGXbKW7vYJIWWRSnn2HepWXxo9CzMSs= -github.com/wago-org/wago v0.0.0-20260812142346-bdfc6aaafa12/go.mod h1:6XmxI3S5qJ+YAzObyXjC6FmxHtbLhBBxrglzix42zl8= +github.com/wago-org/wago v0.0.0-20260812143921-6f5623623b4f h1:3Kb/21SHOb7PrODj0PXgYkwcmG7CGYOEGCGGQIffn1I= +github.com/wago-org/wago v0.0.0-20260812143921-6f5623623b4f/go.mod h1:6XmxI3S5qJ+YAzObyXjC6FmxHtbLhBBxrglzix42zl8= From 019c08851ad42f7956468ccd2c7ba3cb02a9ad23 Mon Sep 17 00:00:00 2001 From: JairusSW Date: Wed, 12 Aug 2026 07:46:39 -0700 Subject: [PATCH 7/7] chore: pin verified plugin runtime --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 0d37c01..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-20260812143921-6f5623623b4f +require github.com/wago-org/wago v0.0.0-20260812144524-1c58c9862d25 diff --git a/go.sum b/go.sum index 62409d4..24b3655 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,2 @@ -github.com/wago-org/wago v0.0.0-20260812143921-6f5623623b4f h1:3Kb/21SHOb7PrODj0PXgYkwcmG7CGYOEGCGGQIffn1I= -github.com/wago-org/wago v0.0.0-20260812143921-6f5623623b4f/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=