From d1f37fa5d6e6f4d9bbf20e14efc8ed3d44814293 Mon Sep 17 00:00:00 2001 From: JairusSW Date: Wed, 12 Aug 2026 06:44:03 -0700 Subject: [PATCH 01/11] plugins: migrate networking providers 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: - functional race tests, full vet, and 12-provider catalog check pass Docs: - README and plugin metadata updated for vNext --- README.md | 145 ++-- agent-todo.md | 7 +- aggregate_compat_test.go | 30 +- compat/compat.go | 4 +- compat/compat_test.go | 6 +- composition_test.go | 12 +- dhcpv4/dhcpv4_test.go | 25 +- dhcpv4/plugin_test.go | 15 + dhcpv4/register/register.go | 19 +- dhcpv4/register/register_test.go | 14 +- dhcpv6/dhcpv6_test.go | 17 +- dhcpv6/plugin_test.go | 15 + dhcpv6/register/register.go | 19 +- dhcpv6/register/register_test.go | 14 +- dns/plugin_test.go | 19 + dns/register/register.go | 21 +- dns/register/register_test.go | 17 +- dns/register_test.go | 55 +- dns_test.go | 82 +- docs/abi-v1.md | 11 +- docs/architecture.md | 92 +- docs/current-plugin-review.md | 3 + docs/protocol-submodule-migration.md | 50 +- docs/release-signoff.md | 10 +- docs/tls.md | 7 +- docs/wago-plugin-plan-compatibility.md | 99 +-- docs/wago-upstream-review.md | 3 + go.mod | 7 +- go.sum | 2 + icmpv4/icmpv4_test.go | 23 +- icmpv4/plugin_test.go | 15 + icmpv4/register/register.go | 20 +- icmpv4/register/register_test.go | 14 +- icmpv6/icmpv6_test.go | 19 +- icmpv6/plugin_test.go | 15 + icmpv6/register/register.go | 19 +- icmpv6/register/register_test.go | 14 +- internal/binding/dhcpv4/descriptor_test.go | 2 - internal/binding/dhcpv4/dhcpv4.go | 2 +- internal/binding/dhcpv6/descriptor_test.go | 2 - internal/binding/dhcpv6/dhcpv6.go | 2 +- internal/binding/dns/descriptor_test.go | 2 - internal/binding/dns/dns.go | 2 +- internal/binding/icmpv4/descriptor_test.go | 2 - internal/binding/icmpv4/icmpv4.go | 2 +- internal/binding/icmpv6/descriptor_test.go | 2 - internal/binding/icmpv6/icmpv6.go | 2 +- internal/binding/ipv6/descriptor_test.go | 2 - internal/binding/ipv6/ipv6.go | 2 +- .../binding/linklocal4/descriptor_test.go | 2 - internal/binding/linklocal4/linklocal4.go | 2 +- internal/binding/mdns/descriptor_test.go | 2 - internal/binding/mdns/mdns.go | 2 +- internal/binding/ntp/descriptor_test.go | 2 - internal/binding/ntp/ntp.go | 2 +- internal/binding/tcp/descriptor_test.go | 2 - internal/binding/tcp/tcp.go | 2 +- internal/binding/tls/descriptor_test.go | 2 - internal/binding/tls/tls.go | 2 +- internal/binding/udp/descriptor_test.go | 2 - internal/binding/udp/udp.go | 2 +- internal/dependencytest/dependencies_test.go | 10 +- internal/dependencytest/inspection_test.go | 8 +- internal/inspectionpolicy/policy.go | 2 +- internal/inspectionpolicy/policy.json | 24 +- internal/inspectionpolicy/policy_test.go | 203 ++++- internal/instance/core/manager.go | 171 ++-- internal/instance/core/manager_test.go | 55 +- internal/plugin/binding_test.go | 2 +- internal/plugin/host.go | 16 +- internal/plugin/registry.go | 41 +- internal/plugin/registry_test.go | 16 +- internal/plugintest/plugintest.go | 126 +++ ipv6/ipv6_test.go | 27 +- ipv6/plugin_test.go | 19 + ipv6/register/register.go | 20 +- ipv6/register/register_test.go | 17 +- lifecycle_test.go | 493 ----------- linklocal4/linklocal4_test.go | 19 +- linklocal4/plugin_test.go | 15 + linklocal4/register/register.go | 19 +- linklocal4/register/register_test.go | 14 +- linklocal4_runtime_test.go | 26 +- mdns/mdns_test.go | 25 +- mdns/plugin_test.go | 15 + mdns/register/register.go | 19 +- mdns/register/register_test.go | 14 +- net.go | 342 +++++--- net_test.go | 155 +--- ntp/ntp_test.go | 25 +- ntp/plugin_test.go | 15 + ntp/register/register.go | 22 +- ntp/register/register_test.go | 14 +- plugin_lifecycle_test.go | 397 +++++++++ plugin_test_helpers_test.go | 100 +++ poll_test.go | 37 +- register/register.go | 72 +- register/register_test.go | 43 +- scripts/custom-cli-signoff.sh | 2 +- scripts/wago-plugin-plan-compat.sh | 110 +-- selective_composition_test.go | 10 +- tcp/plugin_test.go | 19 + tcp/register/register.go | 20 +- tcp/register/register_test.go | 17 +- tcp/register_test.go | 51 +- tcp_test.go | 128 +-- tls/plugin_test.go | 15 + tls/register_test.go | 22 +- udp/plugin_test.go | 19 + udp/register/register.go | 20 +- udp/register/register_test.go | 17 +- udp/register_test.go | 55 +- udp_test.go | 70 +- wago.json | 50 +- wago.providers.json | 795 ++++++++++++++++++ worker_lifecycle_test.go | 488 ----------- 116 files changed, 3302 insertions(+), 2251 deletions(-) create mode 100644 dhcpv4/plugin_test.go create mode 100644 dhcpv6/plugin_test.go create mode 100644 dns/plugin_test.go create mode 100644 icmpv4/plugin_test.go create mode 100644 icmpv6/plugin_test.go create mode 100644 internal/plugintest/plugintest.go create mode 100644 ipv6/plugin_test.go delete mode 100644 lifecycle_test.go create mode 100644 linklocal4/plugin_test.go create mode 100644 mdns/plugin_test.go create mode 100644 ntp/plugin_test.go create mode 100644 plugin_lifecycle_test.go create mode 100644 plugin_test_helpers_test.go create mode 100644 tcp/plugin_test.go create mode 100644 tls/plugin_test.go create mode 100644 udp/plugin_test.go create mode 100644 wago.providers.json delete mode 100644 worker_lifecycle_test.go diff --git a/README.md b/README.md index f3fa949..79e1a86 100644 --- a/README.md +++ b/README.md @@ -65,36 +65,60 @@ UDP-only: truncated responses return `TEMPORARY_FAILURE` because DNS-over-TCP fallback is not implemented. Privileged packet access remains absent and unsupported. -The primary composition API selects only the protocols a runtime should expose: +The conventional `/register` catalogs are explicit and side-effect free. A +generated runtime links the catalog it reviewed and supplies the selections, +definition digests, exact Authority Grants, configuration, and Contract bindings +from `wago-lock.json`: ```go -network := wagonet.New( - wagonet.WithConfig(wagonet.Config{StaticIPv4: deploymentNetwork}), -) -if err := tcp.Register(network); err != nil { - return err +providers := tcpregister.Providers() // github.com/wago-org/net/tcp +set := wago.PluginSet{ + Providers: providers, + Selections: selectionsFromLock, } -return wago.NewRuntime().Use(network) +return runtime.LoadPlugins(ctx, set) ``` -Protocols compose explicitly when a guest needs more than one: +There is no blank-import discovery or process-global registration. An empty +catalog leaves an empty Runtime. The root `register.Providers()` catalog exposes +the reviewed all-protocol provider `github.com/wago-org/net` plus every +published selective provider declared in `wago.json`. Leaf catalogs are +convenient subsets with full package IDs, such as `github.com/wago-org/net/tcp` +and `github.com/wago-org/net/udp`. + +Deployment-specific protocol combinations are one immutable provider, so they +share one exact-instance network state instead of becoming accidentally coupled +plugins: ```go -network := wagonet.New( - wagonet.WithConfig(wagonet.Config{StaticIPv4: deploymentNetwork}), -) -if err := udp.Register(network); err != nil { - return err -} -if err := tcp.Register(network); err != nil { - return err -} -if err := dns.Register(network, dns.Resolver("192.0.2.53")); err != nil { - return err -} -return wago.NewRuntime().Use(network) +provider := wagonet.Provider(wagonet.ProviderSpec{ + ID: "example.com/acme/runtime/network", + Name: "Acme runtime network", + Description: "TCP, UDP, and bounded DNS for the Acme guest", + Modules: []string{ + wagonet.Module, wagonet.TCPModule, wagonet.UDPModule, wagonet.DNSModule, + }, + Factory: func() (*wagonet.Network, error) { + network := wagonet.New( + wagonet.WithConfig(wagonet.Config{StaticIPv4: deploymentNetwork}), + ) + if err := udp.Register(network); err != nil { return nil, err } + if err := tcp.Register(network); err != nil { return nil, err } + if err := dns.Register(network, dns.Resolver("192.0.2.53")); err != nil { + return nil, err + } + return network, nil + }, +}) ``` +The provider requests `host.import.define` for exactly those four modules plus +`host.caller.identify`, `instance.instantiate.intercept`, and +`instance.close.observe`. Registration fails before commit if a required grant +is absent or wider contributions are attempted. The published configuration is +an empty strict object (`additionalProperties: false`); deployment configuration +belongs in the reviewed factory above. + TLS is selected separately and does not imply raw TCP: ```go @@ -112,8 +136,8 @@ if err := wagonettls.Register(network, wagonettls.WithClientProfile(profile)); e } ``` -TLS intentionally has no `tls/register` zero-configuration extension and no -`net-tls` custom-CLI key. Trust roots, verification identities, ALPN, client +TLS intentionally has no `tls/register` catalog provider. Trust roots, +verification identities, ALPN, client credentials, and profile IDs are deployment authority that must be supplied by explicit Go composition; the repository does not invent placeholder TLS policy. @@ -199,7 +223,7 @@ with a separately configured scoped link-local IPv6 identity. Registering only T exposes exactly `net.info` and `net.tls`, `wago_net.abi_version`, and nine `wago_net_tls` imports; it does not expose `net.tcp` or `wago_net_tcp`. This exact TLS surface is inspected through explicit composition fixtures rather -than a self-registering extension. Unregistered protocol imports are absent and fail normal WebAssembly import resolution. The public TCP, +than a zero-policy catalog entry. Unregistered protocol imports are absent and fail normal WebAssembly import resolution. The public TCP, UDP, DNS, ICMPv4, NTP, mDNS, DHCPv4, link-local, IPv6, ICMPv6, DHCPv6, and TLS facades each construct an opaque descriptor, and all twelve checked host tables live in protocol-specific internal binding packages. The @@ -251,16 +275,24 @@ namespace-facet, ABI, and adapter packages. Protocol authority contributions are deep-copied and composed once before manager construction; one immutable policy and quota domain remain shared per exact instance, with deny-wins behavior. -The aggregate advanced compatibility path is now explicit: +The aggregate compatibility constructor can be placed behind a deployment-owned +provider factory: ```go -network := compat.Init(wagonet.Config{ - // Immutable policy, finite quota/readiness, packet-link, static IPv4, - // UDP queue, TCP pool, and bounded DNS resolver settings. +provider := wagonet.Provider(wagonet.ProviderSpec{ + ID: "example.com/acme/runtime/compat-network", + Name: "Acme compatibility network", + Description: "Reviewed aggregate UDP, TCP, and DNS configuration", + Modules: []string{ + wagonet.Module, wagonet.UDPModule, wagonet.TCPModule, wagonet.DNSModule, + }, + Factory: func() (*wagonet.Network, error) { + return compat.Init(wagonet.Config{ + // Immutable policy, finite quota/readiness, packet-link, static + // IPv4, UDP queue, TCP pool, and bounded DNS resolver settings. + }), nil + }, }) -if err := wago.NewRuntime().Use(network); err != nil { - return err -} ``` Import `github.com/wago-org/net/compat` for that constructor. The former root @@ -274,37 +306,52 @@ storage through `tcp.WithConfig(tcp.Config{...})`, `udp.WithConfig(udp.Config{...})`, or `dns.WithConfig(dns.Config{...})`; the compatibility constructor maps the legacy aggregate protocol fields explicitly. -The extension declares that networking state requires physical reinstantiation. -Wago therefore downgrades `ResetMemorySnapshot` and other in-place class reset -requests to `ResetReinstantiate`; UDP/TCP/DNS handles, queues, policy state, and -quota accounts cannot cross leases even when callers request snapshot reuse. - -Custom Wago binaries can self-register exactly one protocol without compiling -the others: +Custom Wago binaries link only the explicit protocol catalogs they need: ```go -import _ "github.com/wago-org/net/tcp/register" // extension key: net-tcp -// or github.com/wago-org/net/udp/register // extension key: net-udp -// or github.com/wago-org/net/dns/register // extension key: net-dns -// or github.com/wago-org/net/ipv6/register // extension key: net-ipv6 -// or github.com/wago-org/net/icmpv6/register // extension key: net-icmpv6 -// or github.com/wago-org/net/dhcpv6/register // extension key: net-dhcpv6 +providers := tcpregister.Providers() +// Provider ID: github.com/wago-org/net/tcp ``` -The root package remains the explicit all-protocol bundle: +The root catalog contains the explicit all-protocol bundle and every published +selective provider (TLS excluded). The lockfile selection chooses the exact ID +to load: ```go -import _ "github.com/wago-org/net/register" // extension key: net +providers := netregister.Providers() +// Includes github.com/wago-org/net and each manifest subpackage ID. ``` The granular packages install protocol defaults but do not invent deployment IPv4 or IPv6 identity/link configuration; DNS resolver storage also remains disabled in -the zero-configuration self-registering form. Applications needing those values +the zero-configuration catalog form. Applications needing those values should use explicit `wagonet.New` composition. Likewise, low-level `InfoImports` and `Imports(Config{})` stay limited to the stateless core ABI-version surface rather than attempting to expose configured protocol resources without exact instance ownership. +## Cross-plugin contract + +Networking provides `github.com/wago-org/net/service` major 1 as the typed +`wagonet.Contract`. A consumer declares both a `PluginRequirement` for the exact +network provider ID and a required `ContractRequirement`, then obtains a +`*plugin.Ref[wagonet.Service]` with `plugin.Require`. Calls must stay inside +`Ref.With`; the reference cannot be retained as an unleased service value. + +`Service.ImportModules` returns a copy of the immutable selected topology. +`Service.Ready(caller)` resolves only the active synchronous guest caller and +does not expose a Runtime, Instance, namespace, or resource handle. Runtime +shutdown rejects new calls, waits for calls already inside `Ref.With`, stops +consumers before the provider, and revokes the reference. Missing dependencies, +unreviewed bindings, incompatible Contract majors, forged callers, and calls +after revocation all fail closed. + +The caller retains the handle for a direct Runtime instance, but Runtime shutdown +logically closes and drains that instance before plugin teardown. Its close +observer releases every network resource and removes the opaque identity; later +calls fail closed and a caller's later `Instance.Close` is an idempotent no-op. +Plugin-managed instance owners likewise close what they own before replacement. + The guest ABI is custom to Wago. It may follow WASI socket semantics where useful, but it is not binary-compatible with WASI Component Model resources. @@ -365,8 +412,8 @@ synthetic linked ready/blocked/tamper/wrong-link interoperability chains without signature bytes, trust keys, production identity, or activation claims; audits unsupported pool topology; runs bounded fuzz smoke, benchmarks, TinyGo, -cross-build, direct/dependency inspection, granular `net-tcp`/`net-udp`/`net-dns` -and aggregate `net` custom CLI inspection, and final clean-tree checks; and +cross-build, direct/dependency inspection, granular `/tcp`, `/udp`, and `/dns` +provider inspection, aggregate root-provider inspection, and final clean-tree checks; and records disposable artifacts under `.wago/release-signoff`: ```sh diff --git a/agent-todo.md b/agent-todo.md index cbc2632..097bf53 100644 --- a/agent-todo.md +++ b/agent-todo.md @@ -1,5 +1,10 @@ # Wago networking implementation ledger +> Historical implementation ledger: pre-vNext Wago pins, registration API +> snippets, and release evidence below are archival. The current explicit +> provider, Authority, lifecycle, and Contract API is documented in `README.md` +> and `docs/architecture.md`. + ## Mission Deliver a production-quality family of capability-gated Wago networking plugins @@ -31,7 +36,7 @@ and lneto as the first backend. ## TLS client slice (July 20, 2026) - Added granular public package `github.com/wago-org/net/tls`; TLS intentionally - has no self-registering package or `net-tls` key because deployment trust and + has no default catalog provider because deployment trust and identity authority must be supplied explicitly by the host. - Explicit TLS-only composition is exactly `net.info`, `net.tls`, `wago_net.abi_version`, and nine `wago_net_tls` imports; raw TCP diff --git a/aggregate_compat_test.go b/aggregate_compat_test.go index b059c18..6457576 100644 --- a/aggregate_compat_test.go +++ b/aggregate_compat_test.go @@ -19,8 +19,8 @@ import ( // Init and the helpers in this file preserve the historical aggregate surface // only for the root package's same-package regression suite. Production callers // use compat.Init or explicit protocol registration. -func Init(config Config) *Extension { - extension := newExtension(config) +func Init(config Config) *Network { + extension := newNetwork(config) if extension.configErr == nil { if err := extension.registerAllProtocols(); err != nil { extension.configErr = err @@ -29,7 +29,7 @@ func Init(config Config) *Extension { return extension } -func (e *Extension) registerAllProtocols() error { +func (e *Network) registerAllProtocols() error { for _, descriptor := range aggregateTestDescriptors(e.config) { if err := e.RegisterModule(descriptor); err != nil { return err @@ -38,9 +38,9 @@ func (e *Extension) registerAllProtocols() error { return nil } -func (e *Extension) registerUDPModule() error { return e.RegisterModule(udpbinding.Descriptor()) } -func (e *Extension) registerTCPModule() error { return e.RegisterModule(tcpbinding.Descriptor()) } -func (e *Extension) registerDNSModule() error { return e.RegisterModule(dnsbinding.Descriptor()) } +func (e *Network) registerUDPModule() error { return e.RegisterModule(udpbinding.Descriptor()) } +func (e *Network) registerTCPModule() error { return e.RegisterModule(tcpbinding.Descriptor()) } +func (e *Network) registerDNSModule() error { return e.RegisterModule(dnsbinding.Descriptor()) } func aggregateTestDescriptors(config Config) []plugin.Module { var udpConfig udpbackend.Config @@ -104,38 +104,38 @@ func protocolTestBindings(bindings []plugin.Binding) []binding { return converted } -func (e *Extension) tcpBindings() []binding { +func (e *Network) tcpBindings() []binding { return protocolTestBindings(tcpbinding.Bindings(plugin.NewHost(e.instanceManager()))) } -func (e *Extension) udpBindings() []binding { +func (e *Network) udpBindings() []binding { return protocolTestBindings(udpbinding.Bindings(plugin.NewHost(e.instanceManager()))) } -func (e *Extension) dnsBindings() []binding { +func (e *Network) dnsBindings() []binding { return protocolTestBindings(dnsbinding.Bindings(plugin.NewHost(e.instanceManager()))) } -func (e *Extension) dnsNamespaceDefault(module wago.HostModule, params, results []uint64) { +func (e *Network) dnsNamespaceDefault(module wago.HostModule, params, results []uint64) { dnsbinding.NamespaceDefault(plugin.NewHost(e.instanceManager()), module, params, results) } -func (e *Extension) dnsResolve(module wago.HostModule, params, results []uint64) { +func (e *Network) dnsResolve(module wago.HostModule, params, results []uint64) { dnsbinding.Resolve(plugin.NewHost(e.instanceManager()), module, params, results) } -func (e *Extension) dnsNext(module wago.HostModule, params, results []uint64) { +func (e *Network) dnsNext(module wago.HostModule, params, results []uint64) { dnsbinding.Next(plugin.NewHost(e.instanceManager()), module, params, results) } -func (e *Extension) dnsCancel(module wago.HostModule, params, results []uint64) { +func (e *Network) dnsCancel(module wago.HostModule, params, results []uint64) { dnsbinding.Cancel(plugin.NewHost(e.instanceManager()), module, params, results) } -func (e *Extension) dnsClose(module wago.HostModule, params, results []uint64) { +func (e *Network) dnsClose(module wago.HostModule, params, results []uint64) { dnsbinding.Close(plugin.NewHost(e.instanceManager()), module, params, results) } -func (e *Extension) dnsPoll(module wago.HostModule, params, results []uint64) { +func (e *Network) dnsPoll(module wago.HostModule, params, results []uint64) { dnsbinding.Poll(plugin.NewHost(e.instanceManager()), module, params, results) } diff --git a/compat/compat.go b/compat/compat.go index 6c5c1e2..fa166e2 100644 --- a/compat/compat.go +++ b/compat/compat.go @@ -1,5 +1,5 @@ // Package compat provides the explicit aggregate UDP, TCP, and DNS networking -// extension for callers migrating from the former root Init constructor. +// builder for callers migrating from the former root Init constructor. // // New selective callers should compose github.com/wago-org/net.New with the // individual tcp.Register, udp.Register, and dns.Register functions instead. @@ -14,7 +14,7 @@ import ( // Init constructs one shared network with UDP, TCP, and DNS all selected. The // supplied root Config retains the advanced aggregate configuration surface. -func Init(config wagonet.Config) *wagonet.Extension { +func Init(config wagonet.Config) *wagonet.Network { network := wagonet.New(wagonet.WithConfig(config)) var udpConfig udp.Config var tcpConfig tcp.Config diff --git a/compat/compat_test.go b/compat/compat_test.go index a948447..0810eb0 100644 --- a/compat/compat_test.go +++ b/compat/compat_test.go @@ -6,13 +6,15 @@ import ( wagonet "github.com/wago-org/net" "github.com/wago-org/net/compat" + "github.com/wago-org/net/internal/plugintest" wago "github.com/wago-org/wago" ) func TestInitRegistersExplicitAggregateSurface(t *testing.T) { runtime := wago.NewRuntime() - if err := runtime.Use(compat.Init(wagonet.Config{})); err != nil { - t.Fatalf("Use: %v", err) + defer runtime.Close() + if err := plugintest.LoadNetwork(runtime, compat.Init(wagonet.Config{})); err != nil { + t.Fatalf("LoadPlugins: %v", err) } wantCapabilities := []wago.Capability{wagonet.CapDNS, wagonet.CapInfo, wagonet.CapTCP, wagonet.CapUDP} diff --git a/composition_test.go b/composition_test.go index 36c8ebc..68cf22b 100644 --- a/composition_test.go +++ b/composition_test.go @@ -8,7 +8,7 @@ import ( wago "github.com/wago-org/wago" "github.com/wago-org/wago/src/core/compiler/wasm" - "github.com/wago-org/wago/testutil/wasmtest" + "github.com/wago-org/wago/tests/wasmtest" ) func TestSelectiveProtocolRegistrationMatrix(t *testing.T) { @@ -47,8 +47,10 @@ func TestSelectiveProtocolRegistrationMatrix(t *testing.T) { } runtime := wago.NewRuntime() - if err := runtime.Use(network); err != nil { - t.Fatalf("Use: %v", err) + if test.protocols != "" { + if err := loadNetwork(runtime, network); err != nil { + t.Fatalf("LoadPlugins: %v", err) + } } if got := runtime.Capabilities(); !reflect.DeepEqual(got, test.capabilities) { t.Fatalf("Capabilities = %v, want %v", got, test.capabilities) @@ -73,7 +75,7 @@ func TestProtocolRegistrationFreezesAndRejectsDuplicates(t *testing.T) { t.Fatalf("duplicate TCP registration = %v", err) } runtime := wago.NewRuntime() - if err := runtime.Use(network); err != nil { + if err := loadNetwork(runtime, network); err != nil { t.Fatalf("Use: %v", err) } if err := network.registerUDPModule(); !errors.Is(err, ErrProtocolRegistrationFrozen) { @@ -87,7 +89,7 @@ func TestUnregisteredProtocolFailsOrdinaryImportResolution(t *testing.T) { t.Fatalf("register TCP: %v", err) } runtime := wago.NewRuntime() - if err := runtime.Use(network); err != nil { + if err := loadNetwork(runtime, network); err != nil { t.Fatalf("Use: %v", err) } diff --git a/dhcpv4/dhcpv4_test.go b/dhcpv4/dhcpv4_test.go index 5814b4c..cd9c9f4 100644 --- a/dhcpv4/dhcpv4_test.go +++ b/dhcpv4/dhcpv4_test.go @@ -10,7 +10,6 @@ import ( wagonet "github.com/wago-org/net" dhcpabi "github.com/wago-org/net/internal/abi/dhcpv4" - dhcpbinding "github.com/wago-org/net/internal/binding/dhcpv4" "github.com/wago-org/net/internal/guest" dhcpns "github.com/wago-org/net/internal/namespace/dhcpv4" "github.com/wago-org/net/internal/resource" @@ -23,7 +22,7 @@ func TestSelectiveRegistrationAndFiniteConfiguration(t *testing.T) { t.Fatal(err) } runtime := wago.NewRuntime() - if err := runtime.Use(network); err != nil { + if err := loadNetwork(runtime, network); err != nil { t.Fatal(err) } if got := runtime.Capabilities(); !reflect.DeepEqual(got, []wago.Capability{wagonet.CapDHCPv4, wagonet.CapInfo}) { @@ -102,16 +101,16 @@ func TestActualBackendExactLeaseLifecycleAndDenyWins(t *testing.T) { t.Fatal(err) } runtime := wago.NewRuntime() - if err := runtime.Use(network); err != nil { + if err := loadNetwork(runtime, network); err != nil { t.Fatal(err) } - module, _ := runtime.Compile([]byte{0x00, 0x61, 0x73, 0x6d, 0x01, 0, 0, 0}) + module, _ := compileImportHarness(runtime) instance, err := runtime.Instantiate(context.Background(), module) if err != nil { t.Fatal(err) } defer instance.Close() - host := hostModule{instance: instance, memory: make([]byte, 512)} + host := hostModule{instance: instance, memory: instance.Memory().Bytes()} if got := callImport(t, runtime, host, "namespace_default", 400); got != guest.StatusOK { t.Fatal(got) } @@ -129,10 +128,10 @@ func TestActualBackendExactLeaseLifecycleAndDenyWins(t *testing.T) { t.Fatal(err) } runtime2 := wago.NewRuntime() - if err := runtime2.Use(allowed); err != nil { + if err := loadNetwork(runtime2, allowed); err != nil { t.Fatal(err) } - module2, err := runtime2.Compile([]byte{0x00, 0x61, 0x73, 0x6d, 0x01, 0, 0, 0}) + module2, err := compileImportHarness(runtime2) if err != nil { t.Fatal(err) } @@ -141,7 +140,7 @@ func TestActualBackendExactLeaseLifecycleAndDenyWins(t *testing.T) { t.Fatal(err) } defer instance2.Close() - host2 := hostModule{instance: instance2, memory: make([]byte, 512)} + host2 := hostModule{instance: instance2, memory: instance2.Memory().Bytes()} if callImport(t, runtime2, host2, "namespace_default", 400) != guest.StatusOK { t.Fatal("namespace") } @@ -165,13 +164,11 @@ func TestActualBackendExactLeaseLifecycleAndDenyWins(t *testing.T) { } } -func callImport(t testing.TB, runtime *wago.Runtime, host hostModule, name string, params ...uint64) guest.Status { +func callImport(t testing.TB, _ *wago.Runtime, host hostModule, name string, params ...uint64) guest.Status { t.Helper() - function, ok := runtime.HostImports()[dhcpbinding.Module+"."+name].(wago.HostFunc) - if !ok { - t.Fatalf("DHCPv4 import %q missing", name) + results, err := host.instance.Invoke(name, params...) + if err != nil || len(results) != 1 { + t.Fatalf("DHCPv4 import %q = %v, %v", name, results, err) } - var results [1]uint64 - function(host, params, results[:]) return guest.Status(int32(results[0])) } diff --git a/dhcpv4/plugin_test.go b/dhcpv4/plugin_test.go new file mode 100644 index 0000000..491205d --- /dev/null +++ b/dhcpv4/plugin_test.go @@ -0,0 +1,15 @@ +package dhcpv4 + +import ( + wagonet "github.com/wago-org/net" + "github.com/wago-org/net/internal/plugintest" + wago "github.com/wago-org/wago" +) + +func loadNetwork(runtime *wago.Runtime, network *wagonet.Network) error { + return plugintest.LoadNetwork(runtime, network) +} + +func compileImportHarness(runtime *wago.Runtime) (*wago.Module, error) { + return plugintest.CompileImportHarness(runtime, wagonet.DHCPv4Module) +} diff --git a/dhcpv4/register/register.go b/dhcpv4/register/register.go index bdc8f73..658b053 100644 --- a/dhcpv4/register/register.go +++ b/dhcpv4/register/register.go @@ -1,4 +1,4 @@ -// Package register self-registers the selective DHCPv4-only Wago networking extension. +// Package register exposes the explicit DHCPv4-only networking catalog entry. package register import ( @@ -7,12 +7,15 @@ import ( wago "github.com/wago-org/wago" ) -func init() { - wago.RegisterExtension("net-dhcpv4", func() wago.Extension { - network := wagonet.New() - if err := dhcpv4.Register(network); err != nil { - panic("wagonet/dhcpv4/register: " + err.Error()) - } - return network +func Provider() wago.PluginProvider { + return wagonet.Provider(wagonet.ProviderSpec{ + ID: "github.com/wago-org/net/dhcpv4", Name: "Wago DHCPv4", Description: "Bounded checked DHCPv4 networking.", + Modules: []string{wagonet.Module, wagonet.DHCPv4Module}, + Factory: func() (*wagonet.Network, error) { + network := wagonet.New() + return network, dhcpv4.Register(network) + }, }) } + +func Providers() []wago.PluginProvider { return []wago.PluginProvider{Provider()} } diff --git a/dhcpv4/register/register_test.go b/dhcpv4/register/register_test.go index 87e3410..db7e3df 100644 --- a/dhcpv4/register/register_test.go +++ b/dhcpv4/register/register_test.go @@ -1,21 +1,25 @@ package register_test import ( + "context" "reflect" "testing" wagonet "github.com/wago-org/net" - _ "github.com/wago-org/net/dhcpv4/register" + netregister "github.com/wago-org/net/dhcpv4/register" + "github.com/wago-org/net/internal/plugintest" wago "github.com/wago-org/wago" ) func TestDHCPv4FactoryHasExactRuntimeSurface(t *testing.T) { - extension, ok := wago.NewExtension("net-dhcpv4") - if !ok { - t.Fatal("DHCPv4-only extension was not registered") + providers := netregister.Providers() + if len(providers) != 1 { + t.Fatalf("Providers = %d, want 1", len(providers)) } + provider := providers[0] runtime := wago.NewRuntime() - if err := runtime.Use(extension); err != nil { + defer runtime.Close() + if err := runtime.LoadPlugins(context.Background(), plugintest.Set(provider)); err != nil { t.Fatal(err) } if got, want := runtime.Capabilities(), []wago.Capability{wagonet.CapDHCPv4, wagonet.CapInfo}; !reflect.DeepEqual(got, want) { diff --git a/dhcpv6/dhcpv6_test.go b/dhcpv6/dhcpv6_test.go index 5ac5faa..1e6f855 100644 --- a/dhcpv6/dhcpv6_test.go +++ b/dhcpv6/dhcpv6_test.go @@ -10,7 +10,6 @@ import ( wagonet "github.com/wago-org/net" dhcpabi "github.com/wago-org/net/internal/abi/dhcpv6" - dhcpbinding "github.com/wago-org/net/internal/binding/dhcpv6" "github.com/wago-org/net/internal/guest" dhcpns "github.com/wago-org/net/internal/namespace/dhcpv6" "github.com/wago-org/net/internal/policy" @@ -105,10 +104,10 @@ func (h hostModule) Instance() *wago.Instance { return h.instance } func instantiate(t testing.TB, network *wagonet.Network) (*wago.Runtime, hostModule) { t.Helper() runtime := wago.NewRuntime() - if err := runtime.Use(network); err != nil { + if err := loadNetwork(runtime, network); err != nil { t.Fatal(err) } - module, err := runtime.Compile([]byte{0, 0x61, 0x73, 0x6d, 1, 0, 0, 0}) + module, err := compileImportHarness(runtime) if err != nil { t.Fatal(err) } @@ -117,15 +116,13 @@ func instantiate(t testing.TB, network *wagonet.Network) (*wago.Runtime, hostMod t.Fatal(err) } t.Cleanup(func() { _ = instance.Close() }) - return runtime, hostModule{instance: instance, memory: make([]byte, 4096)} + return runtime, hostModule{instance: instance, memory: instance.Memory().Bytes()} } -func call(t testing.TB, runtime *wago.Runtime, host hostModule, name string, params ...uint64) guest.Status { +func call(t testing.TB, _ *wago.Runtime, host hostModule, name string, params ...uint64) guest.Status { t.Helper() - fn, ok := runtime.HostImports()[dhcpbinding.Module+"."+name].(wago.HostFunc) - if !ok { - t.Fatalf("import %q missing", name) + results, err := host.instance.Invoke(name, params...) + if err != nil || len(results) != 1 { + t.Fatalf("DHCPv6 import %q = %v, %v", name, results, err) } - var results [1]uint64 - fn(host, params, results[:]) return guest.Status(int32(results[0])) } diff --git a/dhcpv6/plugin_test.go b/dhcpv6/plugin_test.go new file mode 100644 index 0000000..911923c --- /dev/null +++ b/dhcpv6/plugin_test.go @@ -0,0 +1,15 @@ +package dhcpv6 + +import ( + wagonet "github.com/wago-org/net" + "github.com/wago-org/net/internal/plugintest" + wago "github.com/wago-org/wago" +) + +func loadNetwork(runtime *wago.Runtime, network *wagonet.Network) error { + return plugintest.LoadNetwork(runtime, network) +} + +func compileImportHarness(runtime *wago.Runtime) (*wago.Module, error) { + return plugintest.CompileImportHarness(runtime, wagonet.DHCPv6Module) +} diff --git a/dhcpv6/register/register.go b/dhcpv6/register/register.go index 93f805f..e717612 100644 --- a/dhcpv6/register/register.go +++ b/dhcpv6/register/register.go @@ -1,4 +1,4 @@ -// Package register self-registers the selective DHCPv6-only extension. +// Package register exposes the explicit DHCPv6 networking catalog entry. package register import ( @@ -7,12 +7,15 @@ import ( wago "github.com/wago-org/wago" ) -func init() { - wago.RegisterExtension("net-dhcpv6", func() wago.Extension { - network := wagonet.New() - if err := dhcpv6.Register(network); err != nil { - panic("wagonet/dhcpv6/register: " + err.Error()) - } - return network +func Provider() wago.PluginProvider { + return wagonet.Provider(wagonet.ProviderSpec{ + ID: "github.com/wago-org/net/dhcpv6", Name: "Wago DHCPv6", Description: "Bounded checked initial DHCPv6 acquisition.", + Modules: []string{wagonet.Module, wagonet.DHCPv6Module}, + Factory: func() (*wagonet.Network, error) { + network := wagonet.New() + return network, dhcpv6.Register(network) + }, }) } + +func Providers() []wago.PluginProvider { return []wago.PluginProvider{Provider()} } diff --git a/dhcpv6/register/register_test.go b/dhcpv6/register/register_test.go index 071451f..7e79ae6 100644 --- a/dhcpv6/register/register_test.go +++ b/dhcpv6/register/register_test.go @@ -1,21 +1,25 @@ package register_test import ( + "context" "reflect" "testing" wagonet "github.com/wago-org/net" - _ "github.com/wago-org/net/dhcpv6/register" + netregister "github.com/wago-org/net/dhcpv6/register" + "github.com/wago-org/net/internal/plugintest" wago "github.com/wago-org/wago" ) func TestDHCPv6FactoryHasExactRuntimeSurface(t *testing.T) { - extension, ok := wago.NewExtension("net-dhcpv6") - if !ok { - t.Fatal("DHCPv6-only extension was not registered") + providers := netregister.Providers() + if len(providers) != 1 { + t.Fatalf("Providers = %d, want 1", len(providers)) } + provider := providers[0] runtime := wago.NewRuntime() - if err := runtime.Use(extension); err != nil { + defer runtime.Close() + if err := runtime.LoadPlugins(context.Background(), plugintest.Set(provider)); err != nil { t.Fatal(err) } if got, want := runtime.Capabilities(), []wago.Capability{wagonet.CapDHCPv6, wagonet.CapInfo}; !reflect.DeepEqual(got, want) { diff --git a/dns/plugin_test.go b/dns/plugin_test.go new file mode 100644 index 0000000..daa1b27 --- /dev/null +++ b/dns/plugin_test.go @@ -0,0 +1,19 @@ +package dns_test + +import ( + wagonet "github.com/wago-org/net" + "github.com/wago-org/net/internal/plugintest" + wago "github.com/wago-org/wago" +) + +func loadNetwork(runtime *wago.Runtime, network *wagonet.Network) error { + return plugintest.LoadNetwork(runtime, network) +} + +func hasImport(runtime *wago.Runtime, module, name string) bool { + return plugintest.HasImport(runtime, module, name) +} + +func compileImportHarness(runtime *wago.Runtime) (*wago.Module, error) { + return plugintest.CompileImportHarness(runtime, wagonet.DNSModule) +} diff --git a/dns/register/register.go b/dns/register/register.go index eff0547..74323ad 100644 --- a/dns/register/register.go +++ b/dns/register/register.go @@ -1,6 +1,4 @@ -// Package register self-registers the selective DNS-only Wago networking -// extension for custom binaries built by `wago pkg build`. Resolver/storage -// remain disabled until an explicitly configured composition is used. +// Package register exposes the explicit DNS-only networking catalog entry. package register import ( @@ -9,12 +7,15 @@ import ( wago "github.com/wago-org/wago" ) -func init() { - wago.RegisterExtension("net-dns", func() wago.Extension { - network := wagonet.New() - if err := dns.Register(network); err != nil { - panic("wagonet/dns/register: " + err.Error()) - } - return network +func Provider() wago.PluginProvider { + return wagonet.Provider(wagonet.ProviderSpec{ + ID: "github.com/wago-org/net/dns", Name: "Wago DNS", Description: "Bounded checked DNS networking.", + Modules: []string{wagonet.Module, wagonet.DNSModule}, + Factory: func() (*wagonet.Network, error) { + network := wagonet.New() + return network, dns.Register(network) + }, }) } + +func Providers() []wago.PluginProvider { return []wago.PluginProvider{Provider()} } diff --git a/dns/register/register_test.go b/dns/register/register_test.go index df16f32..6dbb4ce 100644 --- a/dns/register/register_test.go +++ b/dns/register/register_test.go @@ -6,20 +6,23 @@ import ( "testing" wagonet "github.com/wago-org/net" - _ "github.com/wago-org/net/dns/register" + netregister "github.com/wago-org/net/dns/register" + "github.com/wago-org/net/internal/plugintest" wago "github.com/wago-org/wago" "github.com/wago-org/wago/src/core/compiler/wasm" - "github.com/wago-org/wago/testutil/wasmtest" + "github.com/wago-org/wago/tests/wasmtest" ) func TestDNSFactoryHasExactRuntimeSurface(t *testing.T) { - extension, ok := wago.NewExtension("net-dns") - if !ok { - t.Fatal("DNS-only extension was not registered") + providers := netregister.Providers() + if len(providers) != 1 { + t.Fatalf("Providers = %d, want 1", len(providers)) } + provider := providers[0] runtime := wago.NewRuntime() - if err := runtime.Use(extension); err != nil { - t.Fatalf("Use: %v", err) + defer runtime.Close() + if err := runtime.LoadPlugins(context.Background(), plugintest.Set(provider)); err != nil { + t.Fatalf("LoadPlugins: %v", err) } wantCapabilities := []wago.Capability{wagonet.CapDNS, wagonet.CapInfo} if got := runtime.Capabilities(); !reflect.DeepEqual(got, wantCapabilities) { diff --git a/dns/register_test.go b/dns/register_test.go index 6bff854..91ccaac 100644 --- a/dns/register_test.go +++ b/dns/register_test.go @@ -14,7 +14,7 @@ import ( dnsns "github.com/wago-org/net/internal/namespace/dns" wago "github.com/wago-org/wago" "github.com/wago-org/wago/src/core/compiler/wasm" - "github.com/wago-org/wago/testutil/wasmtest" + "github.com/wago-org/wago/tests/wasmtest" ) func TestRegisterExposesOnlyDNSAndSharedCore(t *testing.T) { @@ -24,7 +24,7 @@ func TestRegisterExposesOnlyDNSAndSharedCore(t *testing.T) { } runtime := wago.NewRuntime() - if err := runtime.Use(network); err != nil { + if err := loadNetwork(runtime, network); err != nil { t.Fatalf("Use: %v", err) } wantCapabilities := []wago.Capability{wagonet.CapDNS, wagonet.CapInfo} @@ -39,10 +39,10 @@ func TestRegisterExposesOnlyDNSAndSharedCore(t *testing.T) { if !reflect.DeepEqual(imports, wantImports) { t.Fatalf("import modules = %v, want %v", imports, wantImports) } - if _, ok := runtime.HostImports()[wagonet.TCPModule+".namespace_default"]; ok { + if hasImport(runtime, wagonet.TCPModule, "namespace_default") { t.Fatal("TCP import exposed by DNS-only registration") } - if _, ok := runtime.HostImports()[wagonet.UDPModule+".namespace_default"]; ok { + if hasImport(runtime, wagonet.UDPModule, "namespace_default") { t.Fatal("UDP import exposed by DNS-only registration") } } @@ -71,7 +71,7 @@ func TestRegisterRejectsDuplicateInvalidOptionResolverFrozenAndNilNetwork(t *tes t.Fatalf("duplicate registration = %v", err) } runtime := wago.NewRuntime() - if err := runtime.Use(network); err != nil { + if err := loadNetwork(runtime, network); err != nil { t.Fatalf("Use: %v", err) } if err := dns.Register(network); !errors.Is(err, wagonet.ErrProtocolRegistrationFrozen) { @@ -85,29 +85,12 @@ func TestSelectiveDNSBindingUsesExactSharedInstanceState(t *testing.T) { t.Fatalf("Register: %v", err) } runtime := wago.NewRuntime() - if err := runtime.Use(network); err != nil { + if err := loadNetwork(runtime, network); err != nil { t.Fatalf("Use: %v", err) } - module, err := runtime.Compile([]byte{0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00}) - if err != nil { - t.Fatalf("Compile empty module: %v", err) - } - instance, err := runtime.Instantiate(context.Background(), module) - if err != nil { - t.Fatalf("Instantiate: %v", err) - } - defer instance.Close() - - fn, ok := runtime.HostImports()[wagonet.DNSModule+".namespace_default"].(wago.HostFunc) - if !ok { + if !hasImport(runtime, wagonet.DNSModule, "namespace_default") { t.Fatal("selective DNS namespace binding missing") } - host := exactHost{instance: instance, memory: make([]byte, wagonet.HandleV1Size)} - results := []uint64{0} - fn(host, []uint64{0}, results) - if got := wagonet.Status(wago.AsI32(results[0])); got != wagonet.StatusNotSupported { - t.Fatalf("namespace_default without configured namespace = %v", got) - } } func TestDefaultDNSResolverAllowsFiniteQueriesAndCallerDenyWins(t *testing.T) { @@ -122,10 +105,10 @@ func TestDefaultDNSResolverAllowsFiniteQueriesAndCallerDenyWins(t *testing.T) { t.Fatalf("Register: %v", err) } runtime := wago.NewRuntime() - if err := runtime.Use(network); err != nil { + if err := loadNetwork(runtime, network); err != nil { t.Fatalf("Use: %v", err) } - module, err := runtime.Compile([]byte{0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00}) + module, err := compileImportHarness(runtime) if err != nil { t.Fatal(err) } @@ -134,7 +117,7 @@ func TestDefaultDNSResolverAllowsFiniteQueriesAndCallerDenyWins(t *testing.T) { t.Fatalf("Instantiate: %v", err) } defer instance.Close() - host := exactHost{instance: instance, memory: make([]byte, 1024)} + host := exactHost{instance: instance, memory: instance.Memory().Bytes()} if got := callDNS(t, runtime, host, "namespace_default", 900); got != wagonet.StatusOK { t.Fatalf("namespace_default = %v", got) } @@ -163,10 +146,10 @@ func TestDefaultDNSStorageFitsSharedDefaultsAndStopsAtEightQueries(t *testing.T) t.Fatalf("Register: %v", err) } runtime := wago.NewRuntime() - if err := runtime.Use(network); err != nil { + if err := loadNetwork(runtime, network); err != nil { t.Fatalf("Use: %v", err) } - module, err := runtime.Compile([]byte{0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00}) + module, err := compileImportHarness(runtime) if err != nil { t.Fatal(err) } @@ -175,7 +158,7 @@ func TestDefaultDNSStorageFitsSharedDefaultsAndStopsAtEightQueries(t *testing.T) t.Fatalf("Instantiate: %v", err) } defer instance.Close() - host := exactHost{instance: instance, memory: make([]byte, 1024)} + host := exactHost{instance: instance, memory: instance.Memory().Bytes()} if got := callDNS(t, runtime, host, "namespace_default", 900); got != wagonet.StatusOK { t.Fatalf("namespace_default = %v", got) } @@ -204,7 +187,7 @@ func TestDNSRegistrationLeavesTCPAndUDPImportsUnresolved(t *testing.T) { t.Fatalf("Register: %v", err) } runtime := wago.NewRuntime() - if err := runtime.Use(network); err != nil { + if err := loadNetwork(runtime, network); err != nil { t.Fatalf("Use: %v", err) } @@ -237,14 +220,12 @@ type exactHost struct { func (h exactHost) Memory() []byte { return h.memory } func (h exactHost) Instance() *wago.Instance { return h.instance } -func callDNS(t testing.TB, runtime *wago.Runtime, host exactHost, name string, params ...uint64) wagonet.Status { +func callDNS(t testing.TB, _ *wago.Runtime, host exactHost, name string, params ...uint64) wagonet.Status { t.Helper() - fn, ok := runtime.HostImports()[wagonet.DNSModule+"."+name].(wago.HostFunc) - if !ok { - t.Fatalf("DNS import %q missing", name) + results, err := host.instance.Invoke(name, params...) + if err != nil || len(results) != 1 { + t.Fatalf("DNS import %q = %v, %v", name, results, err) } - results := []uint64{0} - fn(host, params, results) return wagonet.Status(wago.AsI32(results[0])) } diff --git a/dns_test.go b/dns_test.go index 37119e5..5f7d289 100644 --- a/dns_test.go +++ b/dns_test.go @@ -19,7 +19,7 @@ import ( "github.com/wago-org/net/internal/resource" wago "github.com/wago-org/wago" "github.com/wago-org/wago/src/core/compiler/wasm" - "github.com/wago-org/wago/testutil/wasmtest" + "github.com/wago-org/wago/tests/wasmtest" ) type guestDNSNamespace struct { @@ -105,12 +105,16 @@ func (q *guestDNSQuery) TryNext() (namespace.DNSRecord, namespace.DNSNext, error func TestDNSBindingsAreRegisteredOnlyAsCompleteTable(t *testing.T) { extension := Init(Config{}) - runtime := runtimeForExtension(t, extension) + runtime := runtimeForNetwork(t, extension) if got := len(extension.dnsBindings()); got != 6 { t.Fatalf("checked DNS bindings = %d, want 6", got) } for _, binding := range extension.dnsBindings() { - if _, ok := runtime.HostImports()[DNSModule+"."+binding.name].(wago.HostFunc); !ok { + found := false + for _, spec := range runtime.ProvidedImports() { + found = found || spec.Module == DNSModule && spec.Name == binding.name + } + if !found { t.Fatalf("registered DNS binding %q missing", binding.name) } } @@ -130,17 +134,22 @@ func TestDNSBindingsAreRegisteredOnlyAsCompleteTable(t *testing.T) { func TestGuestDNSUnavailableNamespaceAndCapabilityGate(t *testing.T) { extension := Init(Config{}) - runtime := runtimeForExtension(t, extension) - instance, err := runtime.Instantiate(context.Background(), emptyModule(t, runtime)) + runtime := runtimeForNetwork(t, extension) + module, err := compileImportHarness(runtime, DNSModule) + if err != nil { + t.Fatal(err) + } + instance, err := runtime.Instantiate(context.Background(), module) if err != nil { t.Fatalf("instantiate empty DNS guest: %v", err) } - host := udpHostModule{instance: instance, memory: bytes.Repeat([]byte{0x5a}, 16)} - before := append([]byte(nil), host.memory...) + host := udpHostModule{instance: instance, memory: instance.Memory().Bytes()} + copy(host.memory[:16], bytes.Repeat([]byte{0x5a}, 16)) + before := append([]byte(nil), host.memory[:16]...) if got := callRegisteredDNS(t, runtime, "namespace_default", host, 0); got != StatusNotSupported { t.Fatalf("DNS namespace without configuration = %v", got) } - if !bytes.Equal(host.memory, before) { + if !bytes.Equal(host.memory[:16], before) { t.Fatal("unavailable DNS namespace mutated output") } _ = instance.Close() @@ -150,7 +159,7 @@ func TestGuestDNSUnavailableNamespaceAndCapabilityGate(t *testing.T) { wasmtest.Section(1, wasmtest.Vec(wasmtest.FuncType([]wasm.ValType{wasm.I32}, []wasm.ValType{wasm.I32}))), wasmtest.Section(2, wasmtest.Vec(importEntry)), ) - module, err := runtime.Compile(wasmBytes) + module, err = runtime.Compile(wasmBytes) if err != nil { t.Fatalf("compile DNS capability module: %v", err) } @@ -166,8 +175,8 @@ func TestGuestDNSUnavailableNamespaceAndCapabilityGate(t *testing.T) { func TestRegisteredGuestDNSActualBackendSmoke(t *testing.T) { extension := Init(actualGuestDNSConfig(103)) - runtime := runtimeForExtension(t, extension) - module, err := runtime.Compile([]byte{0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00}) + runtime := runtimeForNetwork(t, extension) + module, err := compileImportHarness(runtime, DNSModule) if err != nil { t.Fatal(err) } @@ -176,7 +185,7 @@ func TestRegisteredGuestDNSActualBackendSmoke(t *testing.T) { t.Fatal(err) } defer instance.Close() - host := udpHostModule{instance: instance, memory: make([]byte, 1024)} + host := udpHostModule{instance: instance, memory: instance.Memory().Bytes()} if got := callRegisteredDNS(t, runtime, "namespace_default", host, 300); got != StatusOK { t.Fatalf("registered DNS namespace = %v", got) } @@ -308,7 +317,7 @@ func TestGuestDNSRejectsMalformedMemoryBeforeWork(t *testing.T) { func TestActualBackendGuestDNSSuccessPollQuotaAndCleanup(t *testing.T) { extension, instance, host := newActualGuestDNSInstance(t, 81) - state, ok := extension.instanceManager().ForInstance(instance) + state, ok := extension.instanceManager().SingleState() if !ok { t.Fatal("missing actual DNS state") } @@ -403,7 +412,7 @@ func TestActualBackendGuestDNSSuccessPollQuotaAndCleanup(t *testing.T) { if err := instance.Close(); err != nil { t.Fatalf("close actual DNS instance: %v", err) } - if _, exists := extension.instanceManager().ForInstance(instance); exists { + if extension.instanceManager().Len() != 0 { t.Fatal("actual DNS state survived instance close") } if usage, closed := account.Snapshot(); !closed || usage != (quota.Usage{}) { @@ -428,7 +437,7 @@ func TestActualBackendGuestDNSFailureStatuses(t *testing.T) { t.Run(test.name, func(t *testing.T) { extension, instance, host := newActualGuestDNSInstance(t, byte(90+test.rcode)) defer instance.Close() - state, _ := extension.instanceManager().ForInstance(instance) + state, _ := extension.instanceManager().SingleState() namespaceHandle := actualGuestDNSNamespaceHandle(t, extension, host) request := namespace.DNSRequest{Name: "example.com", Types: namespace.DNSRecordsA} query := resolveActualGuestDNS(t, extension, host, namespaceHandle, request) @@ -451,7 +460,7 @@ func TestActualBackendGuestDNSFailureStatuses(t *testing.T) { func TestActualBackendGuestDNSTimeoutCancelPolicyKindsAndIsolation(t *testing.T) { extension, instance, host := newActualGuestDNSInstance(t, 101) defer instance.Close() - state, _ := extension.instanceManager().ForInstance(instance) + state, _ := extension.instanceManager().SingleState() namespaceHandle := actualGuestDNSNamespaceHandle(t, extension, host) request := namespace.DNSRequest{Name: "example.com", Types: namespace.DNSRecordsA} query := resolveActualGuestDNS(t, extension, host, namespaceHandle, request) @@ -543,7 +552,7 @@ func BenchmarkGuestDNSPoll(b *testing.B) { } } -func newGuestDNSHarness(t testing.TB, queries ...*guestDNSQuery) (*Extension, *instance.State, *guestDNSNamespace, udpHostModule) { +func newGuestDNSHarness(t testing.TB, queries ...*guestDNSQuery) (*Network, *instance.State, *guestDNSNamespace, udpHostModule) { t.Helper() backend := &guestDNSNamespace{queries: append([]*guestDNSQuery(nil), queries...)} manager, err := instance.NewManagerConfigured(instance.Config{ @@ -565,7 +574,7 @@ func newGuestDNSHarness(t testing.TB, queries ...*guestDNSQuery) (*Extension, *i if !ok { t.Fatal("missing attached DNS state") } - return &Extension{instances: manager}, state, backend, udpHostModule{instance: wagoInstance, memory: make([]byte, 2048)} + return &Network{instances: manager}, state, backend, udpHostModule{instance: wagoInstance, memory: make([]byte, 2048)} } func callDNS(function wago.HostFunc, host udpHostModule, params ...uint64) Status { @@ -574,17 +583,24 @@ func callDNS(function wago.HostFunc, host udpHostModule, params ...uint64) Statu return Status(int32(results[0])) } -func callRegisteredDNS(t testing.TB, runtime *wago.Runtime, name string, host udpHostModule, params ...uint64) Status { +func callRegisteredDNS(t testing.TB, _ *wago.Runtime, name string, host udpHostModule, params ...uint64) Status { t.Helper() - fn, ok := runtime.HostImports()[DNSModule+"."+name].(wago.HostFunc) - if !ok { - t.Fatalf("registered DNS binding %q missing", name) + results, err := host.instance.Invoke(name, params...) + if err != nil || len(results) != 1 { + t.Fatalf("registered DNS binding %q = %v, %v", name, results, err) } - return callDNS(fn, host, params...) + return Status(wago.AsI32(results[0])) } -func callDNSNamed(t testing.TB, extension *Extension, name string, host udpHostModule, params ...uint64) Status { +func callDNSNamed(t testing.TB, extension *Network, name string, host udpHostModule, params ...uint64) Status { t.Helper() + if host.instance != nil && host.instance.Memory() != nil { + results, err := host.instance.Invoke(name, params...) + if err != nil || len(results) != 1 { + t.Fatalf("DNS import %q = %v, %v", name, results, err) + } + return Status(wago.AsI32(results[0])) + } for _, binding := range extension.dnsBindings() { if binding.name == name { return callDNS(binding.fn, host, params...) @@ -616,11 +632,11 @@ func actualGuestDNSConfig(id byte) Config { } } -func newActualGuestDNSInstance(t testing.TB, id byte) (*Extension, *wago.Instance, udpHostModule) { +func newActualGuestDNSInstance(t testing.TB, id byte) (*Network, *wago.Instance, udpHostModule) { t.Helper() extension := Init(actualGuestDNSConfig(id)) - runtime := runtimeForExtension(t, extension) - module, err := runtime.Compile([]byte{0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00}) + runtime := runtimeForNetwork(t, extension) + module, err := compileImportHarness(runtime, DNSModule) if err != nil { t.Fatalf("compile empty DNS guest: %v", err) } @@ -629,10 +645,10 @@ func newActualGuestDNSInstance(t testing.TB, id byte) (*Extension, *wago.Instanc t.Fatalf("instantiate DNS guest: %v", err) } t.Cleanup(func() { _ = instance.Close() }) - return extension, instance, udpHostModule{instance: instance, memory: make([]byte, 4096)} + return extension, instance, udpHostModule{instance: instance, memory: instance.Memory().Bytes()} } -func actualGuestDNSNamespaceHandle(t testing.TB, extension *Extension, host udpHostModule) resource.Handle { +func actualGuestDNSNamespaceHandle(t testing.TB, extension *Network, host udpHostModule) resource.Handle { t.Helper() if got := callDNSNamed(t, extension, "namespace_default", host, 3000); got != StatusOK { t.Fatalf("DNS namespace_default = %v", got) @@ -640,7 +656,7 @@ func actualGuestDNSNamespaceHandle(t testing.TB, extension *Extension, host udpH return resource.Handle(binary.LittleEndian.Uint64(host.memory[3000:3008])) } -func resolveActualGuestDNS(t testing.TB, extension *Extension, host udpHostModule, namespaceHandle resource.Handle, request namespace.DNSRequest) resource.Handle { +func resolveActualGuestDNS(t testing.TB, extension *Network, host udpHostModule, namespaceHandle resource.Handle, request namespace.DNSRequest) resource.Handle { t.Helper() if !abi.EncodeDNSQueryV1(host.memory, 0, request) { t.Fatalf("encode DNS request %+v", request) @@ -651,7 +667,7 @@ func resolveActualGuestDNS(t testing.TB, extension *Extension, host udpHostModul return resource.Handle(binary.LittleEndian.Uint64(host.memory[300:308])) } -func serviceActualGuestDNSQuery(t testing.TB, extension *Extension, state *instance.State, host udpHostModule) (uint16, uint16) { +func serviceActualGuestDNSQuery(t testing.TB, extension *Network, state *instance.State, host udpHostModule) (uint16, uint16) { t.Helper() frame := serviceActualGuestDNSQueryFrame(t, extension, state, host) if len(frame) < 14+20+8+12 { @@ -665,7 +681,7 @@ func serviceActualGuestDNSQuery(t testing.TB, extension *Extension, state *insta return binary.BigEndian.Uint16(frame[udpOffset+8 : udpOffset+10]), binary.BigEndian.Uint16(frame[udpOffset : udpOffset+2]) } -func serviceActualGuestDNSQueryFrame(t testing.TB, extension *Extension, state *instance.State, host udpHostModule) []byte { +func serviceActualGuestDNSQueryFrame(t testing.TB, extension *Network, state *instance.State, host udpHostModule) []byte { t.Helper() writePollBudget(host.memory, 1000, 2, 2, 1, 1, 1514, 2) if got := callDNSNamed(t, extension, "poll", host, 1100, 2, 1000, 1200); got != StatusOK { @@ -683,7 +699,7 @@ func serviceActualGuestDNSQueryFrame(t testing.TB, extension *Extension, state * return append([]byte(nil), frame[:dequeued.FrameBytes]...) } -func pollActualGuestDNSUntil(t testing.TB, extension *Extension, host udpHostModule, handle resource.Handle, readiness namespace.Readiness) { +func pollActualGuestDNSUntil(t testing.TB, extension *Network, host udpHostModule, handle resource.Handle, readiness namespace.Readiness) { t.Helper() for range 6 { writePollBudget(host.memory, 1000, 2, 2, 1, 1, 1514, 2) diff --git a/docs/abi-v1.md b/docs/abi-v1.md index 9619224..d153459 100644 --- a/docs/abi-v1.md +++ b/docs/abi-v1.md @@ -15,7 +15,7 @@ and the historical zero-config `Imports(Config{})` helper intentionally expose only `wago_net.abi_version`; every resource-owning UDP/TCP/TLS/DNS/ICMPv4/NTP/mDNS/ DHCPv4/link-local/ICMPv6/DHCPv6 imports and the configured IPv6 namespace import require exact Runtime lifecycle identity and are therefore available only through -extension registration. The completed `internal/backend/lneto/core` plus `/tcp`, +explicit provider registration. The completed `internal/backend/lneto/core` plus `/tcp`, `/udp`, and `/dns` adapter extraction and selective opaque contribution assembly change only Go implementation ownership. Unregistered adapters/facets are now absent from the Go dependency graph as well as the Wasm import surface. The @@ -784,12 +784,13 @@ tentative reservation, and instance teardown clears both committed allocations and abandoned reservations. Exact default limits remain implementation policy, not ABI constants. -The host extension requires physical reinstantiation between class leases. -Consequently, a Wago class configured with an in-place reset policy is safely -executed as `ResetReinstantiate` while networking is registered. This is an +Networking state is attached to one opaque Runtime instance identity before +guest start and removed during exact instance close. This is an embedding/lifecycle guarantee rather than a guest-visible status or layout: no handle, queued byte, listener, stream, socket, readiness registration, or quota -token survives into the next lease. +token survives that close boundary. An instance-management plugin must close an +owned instance before replacement; networking does not request management +authority or define a pooling/reset policy. ## Compatibility boundary diff --git a/docs/architecture.md b/docs/architecture.md index 4eca5a8..be7a0f0 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -18,7 +18,7 @@ backend must be able to implement the same guest ABI. ## Import namespace decision -Wago assigns one extension owner to an entire import module. Two extensions cannot +Wago assigns one plugin provider to an entire import module. Two providers cannot independently add functions to the same module under the default collision policy. The suite therefore uses **protocol import modules**: @@ -37,7 +37,7 @@ The suite therefore uses **protocol import modules**: - `wago_net_dhcpv6` for bounded initial DHCPv6 acquisition and copied configuration observations. This permits narrow per-protocol capabilities and independent ABI evolution -without multiple owners competing for `wago_net`. The current root extension is +without multiple owners competing for `wago_net`. The current root provider is the explicit provider for shared per-instance state across its core and complete protocol modules; no process-global state or placeholder protocol module is used. @@ -294,13 +294,14 @@ covers all 4096 selective registrations. Granular `tcp/register`, `udp/register`, `dns/register`, `icmpv4/register`, `ntp/register`, `mdns/register`, `dhcpv4/register`, `linklocal4/register`, `ipv6/register`, `icmpv6/register`, and `dhcpv6/register` packages own only their -selected public facade and exact implementation graph. TLS intentionally has no -self-registering package because no secure zero-configuration extension can +selected public facade and exact implementation graph. Each exports explicit +`Provider` and `Providers` functions and has no registration `init`. TLS +intentionally has no catalog provider because no secure zero-configuration plugin can invent trust roots, identities, ALPN, credentials, or profile IDs. Explicit TLS Go composition may compile the neutral TCP facet and private lneto TCP adapter, but not the public TCP facade, TCP binding, TCP instance operations, or TCP ABI. -The root `register` package intentionally continues to compose the eleven -previously signed-off protocols in one extension rather than using the aggregate +The root `register` package intentionally composes the eleven protocols in one +provider rather than using the aggregate compatibility constructor. TLS remains granular-only until TinyGo and complete release-signoff evidence are refreshed. @@ -316,10 +317,10 @@ path eliminates quota-token allocations; pointer-backed host-module benchmarks now measure the complete UDP and TCP guest poll calls at zero allocations rather than including a value-to-interface boxing artifact. -Each `Extension` owns one private instance-state manager shared by its core, +Each selected networking provider owns one private instance-state manager shared by its core, UDP, TCP, DNS, ICMPv4, NTP, mDNS, DHCPv4, link-local, IPv6, ICMPv6, and DHCPv6 module bindings. Runtime instantiation attaches one resource table, -readiness coordinator, immutable policy, and finite quota ledger to the exact -`*wago.Instance`. Optional static +readiness coordinator, immutable policy, and finite quota ledger to an opaque +`wago.InstanceIdentity`. Optional static IPv4 configuration transactionally reserves namespace quota, constructs the backend, inserts a generation-safe handle, and registers bounded readiness before the state is published. UDP, TCP, DNS, ICMPv4, NTP, mDNS, DHCPv4, link-local, @@ -336,9 +337,11 @@ mutate output. TCP guest bindings prevalidate all complete endpoint, descriptor, payload, result, event, and poll ranges before backend work. Connect and accept roll back newly owned handles if descriptor encoding cannot complete; AGAIN and EOF stream results leave guest outputs unchanged. -Host imports recover exact identity through the additive -`wago.InstanceHostModule` interface, and `BeforeClose` removes the attachment -before polling shutdown, reverse-creation resource cleanup, and quota shutdown. +Host imports recover the active synchronous caller only through the separately +granted `wago.CallerResolver`. A granted instantiate interceptor attaches state +after exact identity exists but before guest start, and a granted close observer +removes the attachment before polling shutdown, reverse-creation resource +cleanup, and quota shutdown. That unpublish-before-close step is intentional: new manager lookups fail closed immediately, while any in-flight `State.WithLock` or `State.Poll` call still holds the per-state lifecycle mutex until its callback returns and teardown can @@ -349,38 +352,34 @@ after those invariants are restored; concurrent detach waiters receive the stable `ErrTeardownPanicked` result, same-instance reattachment waits for record retirement, and unrelated instances continue independently. Panic capture and re-propagation occur in separate ordinary call frames so the same policy works -under both standard Go and TinyGo. The extension also calls -`Registry.RequireReinstantiation`, so class -resets that would reuse a physical instance are engine-downgraded to the same -deterministic close-and-recreate path. Failed later setup and class replacement -use that close path as well. No process-global instance map is used. The low-level -`InfoImports` bundle remains suitable only for stateless core imports such as -`abi_version`; resource-owning protocol extensions require the Runtime -lifecycle path. +under both standard Go and TinyGo. Failed later setup closes the exact instance, +invokes the same detach path, and leaves no published network state. No +process-global instance map is used. The low-level `InfoImports` bundle remains +suitable only for stateless core imports such as `abi_version`; +resource-owning protocol providers require the Runtime lifecycle path. -The companion Wago branch `net/instance-close-hooks` now merges both prerequisite -histories at `97e6f91`: its first parent preserves lifecycle/reset/identity work -through `54499ba`, while its second parent preserves the divergent worker plugin -history at `ffd5ef4b`. Runtime instance metadata carries origin, GC inheritance, -and an optional expiring worker host-call scope outside `Instance`, so the -776-byte instance layout and TinyGo-compatible `HostFunc` shape remain unchanged. -Worker registration is transactional, workers retain finite runtime/queue quotas, -linked parent close waits for child disposal, hook panics cannot skip network -cleanup, and direct or worker host calls still expose exact instance identity. +Callers retain direct Runtime instance handles, but `Runtime.Close` logically +closes and drains every Runtime-created instance before plugin teardown. The +retained handle is closed: networking calls fail, exact close observation has +already released its state, and a later `Instance.Close` is idempotent. This +preserves deterministic cleanup without granting networking instance-management +authority. -## Pool reset enforcement +## Plugin graph and cross-plugin calls -The networking extension declares `Registry.RequireReinstantiation`. A class may -still request `wago.ResetMemorySnapshot`, but `Class.ResetPolicy` reports and -`Lease.Release` enforces `wago.ResetReinstantiate` while networking is registered. -The old physical instance is closed before its fresh replacement is published; -old UDP/TCP/DNS handles become closed in the retired state and fail as -cross-table handles in the new state. Tests rebind UDP/TCP resources on fresh -leases and exercise linked workers whose child instances own all three protocol -kinds. Parent release waits for worker disposal, reverse hooks observe state -before networking detaches it, an isolated hook panic cannot prevent cleanup, -failed callback validation retires the child's state and releases worker quota, -and the next lease receives fresh parent and worker identities. +Every provider requests `host.import.define` only for `wago_net` plus its exact +selected protocol modules. Caller identification, instantiate interception, and +close observation are independent required Authorities. The Runtime validates +the complete definition, grant, contribution, dependency, and Contract graph +before committing registration. + +Networking provides `github.com/wago-org/net/service` major 1. Consumers must +declare an explicit plugin requirement for the chosen provider and bind this +typed Contract. `plugin.Ref.With` is the only supported call path: it holds an +in-flight lease, shutdown rejects new calls and drains existing calls, and the +reference is revoked before provider teardown. The service exposes only copied +module topology and active-caller readiness; it does not expose Runtime, +Instance, namespace, or resource ownership. ## Release gate @@ -388,8 +387,8 @@ and the next lease receives fresh parent and worker identities. pins the merged Wago branch and the lneto/WASI audits, runs standard Go, race, discovered bounded fuzz, every benchmark discovered by `scripts/benchmark-smoke.sh`, TinyGo, a distinct package cross-build, optional -bounded native/QEMU arm64 execution, custom CLI inspection, source-boundary and -plugin-plan compatibility and reviewed-upstream WASI audits, companion +bounded native/QEMU arm64 execution, provider inspection, source-boundary and +reviewed-upstream WASI audits, companion repository tests, and final clean checks. It then exports deterministic non-thin Git packs containing the exact plugin subject and pinned Wago/lneto/WASI commit and source-tree objects, including both ordered Wago parents, before emitting a @@ -407,7 +406,6 @@ known WASI preview-1 native SIGSEGV are documented in `docs/release-signoff.md`. Baseline hosted CI fetches the exact reviewed Wago and lneto commits into ignored dependency worktrees and runs ordinary, shuffled, race, vet, checkptr, and backend-neutral linux/386 checks. Full linux/386 remains -visibly blocked by pinned Wago's missing `runtime.HostCtrlFrameBytes`; production -activation still depends on the stricter publication, arm64-execution, and WASI -release gates. The reviewed newer plugin-plan snapshot requires a separate -identity/cleanup/worker migration and is not a substitute pin. +visibly blocked by the recorded Wago line's missing `runtime.HostCtrlFrameBytes`; +production activation still depends on refreshing the release evidence against +the exact vNext Wago revision, arm64 execution, and the WASI release gates. diff --git a/docs/current-plugin-review.md b/docs/current-plugin-review.md index 48ffa72..56d96a6 100644 --- a/docs/current-plugin-review.md +++ b/docs/current-plugin-review.md @@ -1,5 +1,8 @@ # Current plugin review evidence and publication gate +> Historical pre-vNext review evidence. It does not approve or describe the +> explicit-provider vNext branch; regenerate it before release. + The production networking pin remains the ordered-parent Wago merge `97e6f91e6c822491577faa86f3c30aa5a8fff1e8`. A separate review line proves the same least-authority lifecycle contract on current Wago plugin APIs without diff --git a/docs/protocol-submodule-migration.md b/docs/protocol-submodule-migration.md index 135e8d7..091dae5 100644 --- a/docs/protocol-submodule-migration.md +++ b/docs/protocol-submodule-migration.md @@ -123,23 +123,35 @@ import ( "github.com/wago-org/net/tcp" ) -network := wagonet.New( - wagonet.StaticIPv4(/* deployment identity and link options */), -) -if err := tcp.Register(network); err != nil { - return err -} -return runtime.Use(network) +provider := wagonet.Provider(wagonet.ProviderSpec{ + ID: "example.com/acme/runtime/network", + Name: "Acme network", + Description: "TCP for the Acme guest", + Modules: []string{wagonet.Module, wagonet.TCPModule}, + Factory: func() (*wagonet.Network, error) { + network := wagonet.New(/* deployment identity and link options */) + if err := tcp.Register(network); err != nil { return nil, err } + return network, nil + }, +}) ``` Composition remains explicit: ```go -network := wagonet.New(/* shared namespace options */) -_ = udp.Register(network) -_ = tcp.Register(network) -_ = dns.Register(network, dns.Resolver("192.0.2.53")) -return runtime.Use(network) +provider := wagonet.Provider(wagonet.ProviderSpec{ + ID: "example.com/acme/runtime/network", + Name: "Acme network", + Description: "TCP, UDP, and DNS for the Acme guest", + Modules: []string{wagonet.Module, wagonet.UDPModule, wagonet.TCPModule, wagonet.DNSModule}, + Factory: func() (*wagonet.Network, error) { + network := wagonet.New(/* shared namespace options */) + if err := udp.Register(network); err != nil { return nil, err } + if err := tcp.Register(network); err != nil { return nil, err } + if err := dns.Register(network, dns.Resolver("192.0.2.53")); err != nil { return nil, err } + return network, nil + }, +}) ``` The root package must not import child protocols. Child packages register opaque @@ -219,11 +231,11 @@ assembler, or protocol lneto adapter package. Aggregate callers move to - `internal/abi/core` now contains only checked memory, address/endpoint, handle, and poll layouts; protocol codecs are isolated in `internal/abi/tcp`, `/udp`, and `/dns` and are gated from omitted fixture graphs. -- `register/register.go` intentionally constructs the aggregate extension. +- `register/register.go` intentionally constructs the aggregate provider. Exact runtime registration, protocol implementation compile isolation, finite -client defaults, policy composition, granular register packages, self-register -factory inspection, granular standard-Go/TinyGo custom CLI inspection, practical +client defaults, policy composition, granular explicit provider catalogs, +definition inspection, granular standard-Go/TinyGo inspection, practical fuzz/benchmark smoke, cross-build, and pack-only reconstruction are implemented and passing. The strict local heavyweight signoff passes; production activation remains externally blocked. @@ -295,9 +307,9 @@ authority by default. ### Stage 6: granular packaging and compatibility -Implemented. `tcp/register`, `udp/register`, and `dns/register` self-register -only their protocol. Root `register` explicitly composes all three protocols in -one shared extension. Dependency fixtures reject omitted protocol units from +Implemented. `tcp/register`, `udp/register`, and `dns/register` export explicit +side-effect-free providers for only their protocol. Root `register` explicitly +composes all selected protocols in one shared provider. Dependency fixtures reject omitted protocol units from each granular graph, while the runtime matrix reports exact imports and capabilities for every direct selective combination. @@ -319,7 +331,7 @@ is rejected, as are the former aggregate namespace package and aggregate lneto assembler. The standard, race, vet, fuzz, benchmark, TinyGo, cross-build, lifecycle, -source-boundary, direct/granular dependency, granular custom CLI, source-pack, +source-boundary, direct/granular dependency, granular provider inspection, source-pack, pack-only reconstruction, provenance, and standalone bundle portions pass. The refreshed current-Wago review and selective networking review also pass direct, managed, external-worker, and granular inspection reconstruction. Release diff --git a/docs/release-signoff.md b/docs/release-signoff.md index 4ffb39a..cc1e9cc 100644 --- a/docs/release-signoff.md +++ b/docs/release-signoff.md @@ -1,5 +1,11 @@ # Deterministic release signoff +> [!IMPORTANT] +> This document records the pre-vNext production evidence line. Its Wago pins, +> generated-runtime inspection flow, and release script are historical until the +> evidence is regenerated against the explicit-provider vNext API. They must not +> be used to approve the migrated branch. + The release gate is `scripts/release-signoff.sh`. It runs from a clean plugin checkout with an exact clean production-Wago worktree, clean lneto/WASI inputs, current Wago/networking review worktrees, and the external workers checkout. It @@ -18,8 +24,8 @@ The outbound `net.tls` client is intentionally absent from the aggregate `register` bundle and its historical aggregate evidence. Standard-Go unit, focused race, source-boundary, dependency-isolation, ABI, certificate/hostname, ALPN, truncation, corruption, bounded-queue, quota, and worker-join checks apply -to explicit `tls.Register` composition fixtures separately. TLS has no -self-registering extension and no canonical custom-CLI policy entry because a +to explicit `tls.Register` composition fixtures separately. TLS has no default +catalog provider or canonical generated-runtime policy entry because a zero-configuration bundle would have to invent deployment trust and identity authority. Aggregate completeness therefore remains exact without claiming TLS signoff. TLS must not be called production-ready until the complete release gate diff --git a/docs/tls.md b/docs/tls.md index 001d3e5..27fc92f 100644 --- a/docs/tls.md +++ b/docs/tls.md @@ -36,10 +36,11 @@ loopback gate; raw TCP still requires its own TCP-scoped grant. Multicast and limited broadcast remain unsupported TLS destinations even if advanced policy mentions those endpoint classes. -TLS intentionally has no `tls/register` package or zero-configuration extension. -A self-registering package cannot safely invent trust roots, profile IDs, +TLS intentionally has no `tls/register` catalog provider. An explicit default +provider cannot safely invent trust roots, profile IDs, verification identities, ALPN, or client credentials. Hosts must call -`tls.NewClientProfile` and `tls.Register` explicitly in Go composition. +`tls.NewClientProfile` and `tls.Register` in a deployment-owned +`wagonet.Provider` factory. ## Nonblocking engine diff --git a/docs/wago-plugin-plan-compatibility.md b/docs/wago-plugin-plan-compatibility.md index 01ff31d..7118813 100644 --- a/docs/wago-plugin-plan-compatibility.md +++ b/docs/wago-plugin-plan-compatibility.md @@ -1,75 +1,44 @@ -# Wago plugin-plan compatibility audit +# Wago vNext plugin compatibility -This audit compares networking's exact Wago prerequisite -`97e6f91e6c822491577faa86f3c30aa5a8fff1e8` with the fetched -`origin/plugin-improvements` snapshot -`07a70b58ff26d2c8c49b5f879e7733cb375ec13f`. The latter includes current Wago -main as observed at `7794acc82692aac4ff98756a46a017d0d8768087` and retains the -reviewed worker parent `ffd5ef4`, but it does not contain the exact networking -merge. +The earlier plugin-plan compatibility audit is superseded by the breaking vNext +plugin migration. Networking now targets the explicit-provider API in the Wago +revision selected by `go.mod`; the old production pin is not API-compatible and +must not be used to build this branch. -Run the executable evidence check after fetching Wago refs: +## Required Wago surface -```sh -git -C .audit/wago fetch --all --prune -REQUIRE_CURRENT_PLUGIN_PLAN=1 scripts/wago-plugin-plan-compat.sh -``` - -Without `REQUIRE_CURRENT_PLUGIN_PLAN=1`, the script audits the exact reviewed -objects and reports later movement of the remote-tracking heads. This makes the -committed release gate reproducible while ensuring a new moving-head review is -an explicit operation. - -## Compatibility matrix +Networking depends on these narrow integration seams: -| Networking requirement | `97e6f91` | `07a70b5` | Decision | -|---|---|---|---| -| Runtime-owned before/after close hooks | Present | Present through the capability-scoped lifecycle API | Retained concept, registration migration required | -| Exact synchronous caller identity | Public optional `InstanceHostModule` | Private scoped host module, resolved through `InstanceManager.Caller` | Source-incompatible; a least-authority public resolver is required | -| Reset safety for extension-owned state | `Registry.RequireReinstantiation` dynamically prevents pooled reuse | `RequireReinstantiation`, `Class`, and pool implementation removed | No drop-in API; prove all replacement ownership paths physically close instances | -| Worker lifecycle and finite queues | Reviewed `Workers`, worker origins, linked-parent close, and worker tests | Worker API/tests removed; generic `ManagedInstance` ownership introduced | Semantic migration and new networking tests required | -| Close-hook panic isolation | Hook panics are converted to errors and cannot skip later hooks or resource release | Close hooks are invoked directly; the reviewed panic-isolated runner is absent | Blocking regression for deterministic networking cleanup | -| Declarative plugin authority | No plugin-plan grant layer | `PluginPlan`, explicit host/lifecycle/manage capabilities, budgets, services | Desirable, but manifests and registration must declare exact powers | -| Exact networking merge topology | Exact two-parent merge | Not an ancestor | Cannot replace immutable publication of `97e6f91` | +- `PluginProvider`, immutable `PluginDefinition`, and transactional `Registrar`; +- `host.import.define` grants scoped to `wago_net` and the selected protocol + modules only; +- `host.caller.identify` through an expiring `CallerResolver`; +- `instance.instantiate.intercept` to attach exact-instance state after identity + exists and before guest start; +- `instance.close.observe` to unpublish and close that state; +- typed Contracts and callback-scoped `plugin.Ref.With`; and +- graph validation for explicit requirements, definition digests, Authority + Grants, configuration, and Contract bindings. -The current networking production source depends directly on two APIs absent -from the redesign: `Registry.RequireReinstantiation` in `net.go` and -`InstanceHostModule` in `internal/instance/core/manager.go`. It therefore does not -compile unchanged against `07a70b5`. More importantly, source adaptation alone -would be insufficient because the redesign removed the reviewed worker/class -model and currently lacks the close-hook panic containment that ensures one -extension cannot prevent networking teardown. +Networking does not request instance-management authority and never receives a +raw Runtime or Instance through an observation callback. The opaque identity +used by instantiate, synchronous caller resolution, and close is the sole key +for production state. Repository-local unit fixtures retain a separate direct +pointer path that is not wired into plugin registration. -## Migration decision +## Executable check -**Do not move or rewrite the Wago pin.** Publish `97e6f91` unchanged at an -immutable fetchable ref first. Treat plugin-plan integration as a separate Wago -and networking migration with its own reviewed merge and pin update. +Run: -A valid migration must provide all of the following before networking can adopt -it: - -1. a public, least-authority way for an ordinary host import to resolve the exact - active `*wago.Instance` without exposing runtime internals or retaining caller - authority beyond the synchronous call; -2. panic-isolated before/after close execution that continues deterministic - cleanup, preserves reverse order and shared metadata, and reports failures; -3. proof that every direct and managed ownership path closes extension-owned - state exactly once, with no snapshot/class/managed reuse path bypassing close; -4. capability declarations for host imports and instance lifecycle, without - requesting `instance.manage` merely to recover ordinary caller identity; -5. replacements for the linked worker tests covering exact child identity, - parent/child close ordering, finite ownership, callback failure rollback, and - UDP/TCP/DNS state retirement; and -6. standard Go, race, TinyGo, package inspection, and full networking release - signoff against the resulting immutable Wago revision. +```sh +WAGO_DIR=/path/to/exact/wago scripts/wago-plugin-plan-compat.sh +``` -`InstanceManager.Caller` is useful evidence that the redesign has an expiring -exact-caller primitive, but requiring the networking plugin to own a managed -instance service solely for identity would over-grant authority. Networking -should request only host-import and lifecycle powers unless it actually creates -managed guest instances. +The check validates the exact vNext symbols in the selected Wago source and the +matching networking registrations. It does not fetch refs, accept a moving +branch, or reinterpret an older manifest/API as vNext. -This compatibility audit does not claim the redesign is generally defective; it -records that the reviewed snapshot is not yet a production-compatible substitute -for networking's narrower, already-tested lifecycle/identity/worker contract. +Compatibility is established only together with standard tests, race tests, +`go vet`, strict v1 manifest validation, explicit catalog inspection, exact +Authority-scope tests, provider-to-consumer Contract tests, in-flight shutdown, +revocation, and instance attach/detach rollback. diff --git a/docs/wago-upstream-review.md b/docs/wago-upstream-review.md index 80e518d..dbe671e 100644 --- a/docs/wago-upstream-review.md +++ b/docs/wago-upstream-review.md @@ -1,5 +1,8 @@ # Wago merged lifecycle/worker upstream review +> Historical pre-vNext review evidence. The migrated networking provider uses +> the exact Wago revision selected by `go.mod`, not the merge recorded below. + Networking currently depends on the exact Wago merge object `97e6f91e6c822491577faa86f3c30aa5a8fff1e8`. This object must be published without rebasing, squashing, or replacing either parent: diff --git a/go.mod b/go.mod index 2ecb7f5..3bac584 100644 --- a/go.mod +++ b/go.mod @@ -4,10 +4,5 @@ go 1.24 require ( github.com/soypat/lneto v0.0.0-20260710133615-ab1a0c735a8b - github.com/wago-org/wago v0.1.0 + github.com/wago-org/wago v0.0.0-20260812133922-747a0520edb8 ) - -// Wago has not published v0.1.0 yet. Keep the plugin checkout beside a Wago -// checkout for development; dependency-module replace directives are ignored by -// `wago pkg build`, whose generated build module supplies the engine source. -replace github.com/wago-org/wago => ../wago diff --git a/go.sum b/go.sum index bd1fcb6..ff50bda 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,4 @@ github.com/soypat/lneto v0.0.0-20260710133615-ab1a0c735a8b h1:qcoYcwWsFVI7zu0SRqhGD+B07R8o+sLFHwZQ/LtsHkM= github.com/soypat/lneto v0.0.0-20260710133615-ab1a0c735a8b/go.mod h1:Be5PjwoYukvHFiUXxpYi8+ppH2F/gw/vjGBvFdv+Ti8= +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/icmpv4/icmpv4_test.go b/icmpv4/icmpv4_test.go index b9ddd58..459887d 100644 --- a/icmpv4/icmpv4_test.go +++ b/icmpv4/icmpv4_test.go @@ -9,7 +9,6 @@ import ( wagonet "github.com/wago-org/net" icmpabi "github.com/wago-org/net/internal/abi/icmpv4" - icmpbinding "github.com/wago-org/net/internal/binding/icmpv4" "github.com/wago-org/net/internal/guest" nscore "github.com/wago-org/net/internal/namespace/core" "github.com/wago-org/net/internal/resource" @@ -41,7 +40,7 @@ func TestSelectiveICMPv4RegistrationAndActualBackendLifecycle(t *testing.T) { } runtime := wago.NewRuntime() - if err := runtime.Use(network); err != nil { + if err := loadNetwork(runtime, network); err != nil { t.Fatal(err) } imports := 0 @@ -64,7 +63,7 @@ func TestSelectiveICMPv4RegistrationAndActualBackendLifecycle(t *testing.T) { t.Fatal("ICMPv4 capability not advertised") } - module, err := runtime.Compile([]byte{0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00}) + module, err := compileImportHarness(runtime) if err != nil { t.Fatal(err) } @@ -73,7 +72,7 @@ func TestSelectiveICMPv4RegistrationAndActualBackendLifecycle(t *testing.T) { t.Fatal(err) } defer instance.Close() - host := hostModule{instance: instance, memory: make([]byte, 512)} + host := hostModule{instance: instance, memory: instance.Memory().Bytes()} if got := callImport(t, runtime, host, "namespace_default", 80); got != guest.StatusOK { t.Fatalf("namespace_default = %v", got) } @@ -146,16 +145,16 @@ func TestICMPv4DenyWinsAndCheckedMemoryPrecedesWork(t *testing.T) { t.Fatal(err) } runtime := wago.NewRuntime() - if err := runtime.Use(network); err != nil { + if err := loadNetwork(runtime, network); err != nil { t.Fatal(err) } - module, _ := runtime.Compile([]byte{0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00}) + module, _ := compileImportHarness(runtime) instance, err := runtime.Instantiate(context.Background(), module) if err != nil { t.Fatal(err) } defer instance.Close() - host := hostModule{instance: instance, memory: make([]byte, 256)} + host := hostModule{instance: instance, memory: instance.Memory().Bytes()} if got := callImport(t, runtime, host, "namespace_default", 80); got != guest.StatusOK { t.Fatal(got) } @@ -191,13 +190,11 @@ func TestICMPv4OptionsAreFiniteAndConspicuous(t *testing.T) { } } -func callImport(t testing.TB, runtime *wago.Runtime, host hostModule, name string, params ...uint64) guest.Status { +func callImport(t testing.TB, _ *wago.Runtime, host hostModule, name string, params ...uint64) guest.Status { t.Helper() - function, ok := runtime.HostImports()[icmpbinding.Module+"."+name].(wago.HostFunc) - if !ok { - t.Fatalf("ICMPv4 import %q missing", name) + results, err := host.instance.Invoke(name, params...) + if err != nil || len(results) != 1 { + t.Fatalf("ICMPv4 import %q = %v, %v", name, results, err) } - var results [1]uint64 - function(host, params, results[:]) return guest.Status(int32(results[0])) } diff --git a/icmpv4/plugin_test.go b/icmpv4/plugin_test.go new file mode 100644 index 0000000..3dfac4d --- /dev/null +++ b/icmpv4/plugin_test.go @@ -0,0 +1,15 @@ +package icmpv4 + +import ( + wagonet "github.com/wago-org/net" + "github.com/wago-org/net/internal/plugintest" + wago "github.com/wago-org/wago" +) + +func loadNetwork(runtime *wago.Runtime, network *wagonet.Network) error { + return plugintest.LoadNetwork(runtime, network) +} + +func compileImportHarness(runtime *wago.Runtime) (*wago.Module, error) { + return plugintest.CompileImportHarness(runtime, wagonet.ICMPv4Module) +} diff --git a/icmpv4/register/register.go b/icmpv4/register/register.go index b0f2ead..33ee625 100644 --- a/icmpv4/register/register.go +++ b/icmpv4/register/register.go @@ -1,5 +1,4 @@ -// Package register self-registers the selective ICMPv4-only Wago networking -// extension for custom binaries built by `wago pkg build`. +// Package register exposes the explicit ICMPv4-only networking catalog entry. package register import ( @@ -8,12 +7,15 @@ import ( wago "github.com/wago-org/wago" ) -func init() { - wago.RegisterExtension("net-icmpv4", func() wago.Extension { - network := wagonet.New() - if err := icmpv4.Register(network); err != nil { - panic("wagonet/icmpv4/register: " + err.Error()) - } - return network +func Provider() wago.PluginProvider { + return wagonet.Provider(wagonet.ProviderSpec{ + ID: "github.com/wago-org/net/icmpv4", Name: "Wago ICMPv4", Description: "Bounded checked ICMPv4 networking.", + Modules: []string{wagonet.Module, wagonet.ICMPv4Module}, + Factory: func() (*wagonet.Network, error) { + network := wagonet.New() + return network, icmpv4.Register(network) + }, }) } + +func Providers() []wago.PluginProvider { return []wago.PluginProvider{Provider()} } diff --git a/icmpv4/register/register_test.go b/icmpv4/register/register_test.go index 0e27305..d360257 100644 --- a/icmpv4/register/register_test.go +++ b/icmpv4/register/register_test.go @@ -1,21 +1,25 @@ package register_test import ( + "context" "reflect" "testing" wagonet "github.com/wago-org/net" - _ "github.com/wago-org/net/icmpv4/register" + netregister "github.com/wago-org/net/icmpv4/register" + "github.com/wago-org/net/internal/plugintest" wago "github.com/wago-org/wago" ) func TestICMPv4FactoryHasExactRuntimeSurface(t *testing.T) { - extension, ok := wago.NewExtension("net-icmpv4") - if !ok { - t.Fatal("ICMPv4-only extension was not registered") + providers := netregister.Providers() + if len(providers) != 1 { + t.Fatalf("Providers = %d, want 1", len(providers)) } + provider := providers[0] runtime := wago.NewRuntime() - if err := runtime.Use(extension); err != nil { + defer runtime.Close() + if err := runtime.LoadPlugins(context.Background(), plugintest.Set(provider)); err != nil { t.Fatal(err) } if got, want := runtime.Capabilities(), []wago.Capability{wagonet.CapICMPv4, wagonet.CapInfo}; !reflect.DeepEqual(got, want) { diff --git a/icmpv6/icmpv6_test.go b/icmpv6/icmpv6_test.go index 88eacea..d8d12c5 100644 --- a/icmpv6/icmpv6_test.go +++ b/icmpv6/icmpv6_test.go @@ -10,7 +10,6 @@ import ( wagonet "github.com/wago-org/net" icmpabi "github.com/wago-org/net/internal/abi/icmpv6" - icmpbinding "github.com/wago-org/net/internal/binding/icmpv6" "github.com/wago-org/net/internal/guest" nscore "github.com/wago-org/net/internal/namespace/core" icmpns "github.com/wago-org/net/internal/namespace/icmpv6" @@ -28,7 +27,7 @@ func TestSelectiveRegistrationSurface(t *testing.T) { t.Fatal(err) } runtime := wago.NewRuntime() - if err := runtime.Use(network); err != nil { + if err := loadNetwork(runtime, network); err != nil { t.Fatal(err) } if got := runtime.Capabilities(); !reflect.DeepEqual(got, []wago.Capability{wagonet.CapICMPv6, wagonet.CapInfo}) { @@ -171,10 +170,10 @@ func TestCheckedSeedLookupEchoCancelAndDisabledTruth(t *testing.T) { func instantiate(t testing.TB, network *wagonet.Network) (*wago.Runtime, hostModule) { t.Helper() runtime := wago.NewRuntime() - if err := runtime.Use(network); err != nil { + if err := loadNetwork(runtime, network); err != nil { t.Fatal(err) } - module, err := runtime.Compile([]byte{0x00, 0x61, 0x73, 0x6d, 0x01, 0, 0, 0}) + module, err := compileImportHarness(runtime) if err != nil { t.Fatal(err) } @@ -183,16 +182,14 @@ func instantiate(t testing.TB, network *wagonet.Network) (*wago.Runtime, hostMod t.Fatal(err) } t.Cleanup(func() { _ = instance.Close() }) - return runtime, hostModule{instance: instance, memory: make([]byte, 512)} + return runtime, hostModule{instance: instance, memory: instance.Memory().Bytes()} } -func call(t testing.TB, runtime *wago.Runtime, host hostModule, name string, params ...uint64) guest.Status { +func call(t testing.TB, _ *wago.Runtime, host hostModule, name string, params ...uint64) guest.Status { t.Helper() - function, ok := runtime.HostImports()[icmpbinding.Module+"."+name].(wago.HostFunc) - if !ok { - t.Fatalf("ICMPv6 import %q missing", name) + results, err := host.instance.Invoke(name, params...) + if err != nil || len(results) != 1 { + t.Fatalf("ICMPv6 import %q = %v, %v", name, results, err) } - var results [1]uint64 - function(host, params, results[:]) return guest.Status(int32(results[0])) } diff --git a/icmpv6/plugin_test.go b/icmpv6/plugin_test.go new file mode 100644 index 0000000..8d713bf --- /dev/null +++ b/icmpv6/plugin_test.go @@ -0,0 +1,15 @@ +package icmpv6 + +import ( + wagonet "github.com/wago-org/net" + "github.com/wago-org/net/internal/plugintest" + wago "github.com/wago-org/wago" +) + +func loadNetwork(runtime *wago.Runtime, network *wagonet.Network) error { + return plugintest.LoadNetwork(runtime, network) +} + +func compileImportHarness(runtime *wago.Runtime) (*wago.Module, error) { + return plugintest.CompileImportHarness(runtime, wagonet.ICMPv6Module) +} diff --git a/icmpv6/register/register.go b/icmpv6/register/register.go index 5ee03b2..145df80 100644 --- a/icmpv6/register/register.go +++ b/icmpv6/register/register.go @@ -1,4 +1,4 @@ -// Package register self-registers the selective ICMPv6/NDP extension. +// Package register exposes the explicit ICMPv6 networking catalog entry. package register import ( @@ -7,12 +7,15 @@ import ( wago "github.com/wago-org/wago" ) -func init() { - wago.RegisterExtension("net-icmpv6", func() wago.Extension { - network := wagonet.New() - if err := icmpv6.Register(network); err != nil { - panic("wagonet/icmpv6/register: " + err.Error()) - } - return network +func Provider() wago.PluginProvider { + return wagonet.Provider(wagonet.ProviderSpec{ + ID: "github.com/wago-org/net/icmpv6", Name: "Wago ICMPv6", Description: "Bounded checked ICMPv6 and neighbor discovery.", + Modules: []string{wagonet.Module, wagonet.ICMPv6Module}, + Factory: func() (*wagonet.Network, error) { + network := wagonet.New() + return network, icmpv6.Register(network) + }, }) } + +func Providers() []wago.PluginProvider { return []wago.PluginProvider{Provider()} } diff --git a/icmpv6/register/register_test.go b/icmpv6/register/register_test.go index db29c98..51b37b8 100644 --- a/icmpv6/register/register_test.go +++ b/icmpv6/register/register_test.go @@ -1,20 +1,24 @@ package register_test import ( + "context" "testing" wagonet "github.com/wago-org/net" - _ "github.com/wago-org/net/icmpv6/register" + netregister "github.com/wago-org/net/icmpv6/register" + "github.com/wago-org/net/internal/plugintest" wago "github.com/wago-org/wago" ) func TestGranularFactory(t *testing.T) { - extension, ok := wago.NewExtension("net-icmpv6") - if !ok { - t.Fatal("net-icmpv6 extension absent") + providers := netregister.Providers() + if len(providers) != 1 { + t.Fatalf("Providers = %d, want 1", len(providers)) } + provider := providers[0] runtime := wago.NewRuntime() - if err := runtime.Use(extension); err != nil { + defer runtime.Close() + if err := runtime.LoadPlugins(context.Background(), plugintest.Set(provider)); err != nil { t.Fatal(err) } imports := 0 diff --git a/internal/binding/dhcpv4/descriptor_test.go b/internal/binding/dhcpv4/descriptor_test.go index 0082176..9d949fc 100644 --- a/internal/binding/dhcpv4/descriptor_test.go +++ b/internal/binding/dhcpv4/descriptor_test.go @@ -6,7 +6,6 @@ import ( "github.com/wago-org/net/internal/guest" nscore "github.com/wago-org/net/internal/namespace/core" "github.com/wago-org/net/internal/plugin" - wago "github.com/wago-org/wago" ) func TestDescriptorInstallsCompleteBindingsAndPreservesBackend(t *testing.T) { @@ -26,7 +25,6 @@ func TestDescriptorInstallsCompleteBindingsAndPreservesBackend(t *testing.T) { }, ) descriptor := Descriptor(backend) - descriptor.Install(new(wago.Registry), plugin.Host{}) if err := descriptor.ConfigureBackend(plugin.BackendLnetoV1, base); err != nil || !configured { t.Fatalf("ConfigureBackend = %v configured=%v", err, configured) } diff --git a/internal/binding/dhcpv4/dhcpv4.go b/internal/binding/dhcpv4/dhcpv4.go index 5a091d0..40defff 100644 --- a/internal/binding/dhcpv4/dhcpv4.go +++ b/internal/binding/dhcpv4/dhcpv4.go @@ -19,7 +19,7 @@ const ( ) func Descriptor(backend ...plugin.Backend) plugin.Module { - return plugin.NewModule(plugin.ModuleDHCPv4, func(registry *wago.Registry, host plugin.Host) { + return plugin.NewModule(plugin.ModuleDHCPv4, func(registry *plugin.Registrar, host plugin.Host) { registry.Capability(Capability, wago.CapabilityDocs("use bounded DHCPv4 client leases and explicitly configured finite server service")) plugin.RegisterBindings(registry.ImportModule(Module), Bindings(host)) }, backend...) diff --git a/internal/binding/dhcpv6/descriptor_test.go b/internal/binding/dhcpv6/descriptor_test.go index b3b90ca..546cfd6 100644 --- a/internal/binding/dhcpv6/descriptor_test.go +++ b/internal/binding/dhcpv6/descriptor_test.go @@ -6,7 +6,6 @@ import ( "github.com/wago-org/net/internal/guest" nscore "github.com/wago-org/net/internal/namespace/core" "github.com/wago-org/net/internal/plugin" - wago "github.com/wago-org/wago" ) func TestDescriptorInstallsCompleteBindingsAndPreservesBackend(t *testing.T) { @@ -26,7 +25,6 @@ func TestDescriptorInstallsCompleteBindingsAndPreservesBackend(t *testing.T) { }, ) descriptor := Descriptor(backend) - descriptor.Install(new(wago.Registry), plugin.Host{}) if err := descriptor.ConfigureBackend(plugin.BackendLnetoV1, base); err != nil || !configured { t.Fatalf("ConfigureBackend = %v configured=%v", err, configured) } diff --git a/internal/binding/dhcpv6/dhcpv6.go b/internal/binding/dhcpv6/dhcpv6.go index 7bf4ef7..e181312 100644 --- a/internal/binding/dhcpv6/dhcpv6.go +++ b/internal/binding/dhcpv6/dhcpv6.go @@ -19,7 +19,7 @@ const ( ) func Descriptor(backend ...plugin.Backend) plugin.Module { - return plugin.NewModule(plugin.ModuleDHCPv6, func(registry *wago.Registry, host plugin.Host) { + return plugin.NewModule(plugin.ModuleDHCPv6, func(registry *plugin.Registrar, host plugin.Host) { registry.Capability(Capability, wago.CapabilityDocs("use the bounded checked initial DHCPv6 client acquisition subset")) plugin.RegisterBindings(registry.ImportModule(Module), Bindings(host)) }, backend...) diff --git a/internal/binding/dns/descriptor_test.go b/internal/binding/dns/descriptor_test.go index 859a585..929edd2 100644 --- a/internal/binding/dns/descriptor_test.go +++ b/internal/binding/dns/descriptor_test.go @@ -7,7 +7,6 @@ import ( "github.com/wago-org/net/internal/guest" nscore "github.com/wago-org/net/internal/namespace/core" "github.com/wago-org/net/internal/plugin" - wago "github.com/wago-org/wago" ) func TestDescriptorInstallsCompleteBindingsAndPreservesBackend(t *testing.T) { @@ -27,7 +26,6 @@ func TestDescriptorInstallsCompleteBindingsAndPreservesBackend(t *testing.T) { }, ) descriptor := Descriptor(backend) - descriptor.Install(new(wago.Registry), plugin.Host{}) if err := descriptor.ConfigureBackend(plugin.BackendLnetoV1, base); err != nil || !configured { t.Fatalf("ConfigureBackend = %v configured=%v", err, configured) } diff --git a/internal/binding/dns/dns.go b/internal/binding/dns/dns.go index 498f908..163d6ef 100644 --- a/internal/binding/dns/dns.go +++ b/internal/binding/dns/dns.go @@ -23,7 +23,7 @@ const ( // Descriptor returns the opaque DNS module installed by the public dns facade // and the bounded aggregate compatibility path. func Descriptor(backend ...plugin.Backend) plugin.Module { - return plugin.NewModule(plugin.ModuleDNS, func(registry *wago.Registry, host plugin.Host) { + return plugin.NewModule(plugin.ModuleDNS, func(registry *plugin.Registrar, host plugin.Host) { registry.Capability(Capability, wago.CapabilityDocs("use checked bounded DNS queries for the exact calling instance")) plugin.RegisterBindings(registry.ImportModule(Module), Bindings(host)) }, backend...) diff --git a/internal/binding/icmpv4/descriptor_test.go b/internal/binding/icmpv4/descriptor_test.go index 4cbe98b..71dc91b 100644 --- a/internal/binding/icmpv4/descriptor_test.go +++ b/internal/binding/icmpv4/descriptor_test.go @@ -6,7 +6,6 @@ import ( "github.com/wago-org/net/internal/guest" nscore "github.com/wago-org/net/internal/namespace/core" "github.com/wago-org/net/internal/plugin" - wago "github.com/wago-org/wago" ) func TestDescriptorInstallsCompleteBindingsAndPreservesBackend(t *testing.T) { @@ -26,7 +25,6 @@ func TestDescriptorInstallsCompleteBindingsAndPreservesBackend(t *testing.T) { }, ) descriptor := Descriptor(backend) - descriptor.Install(new(wago.Registry), plugin.Host{}) if err := descriptor.ConfigureBackend(plugin.BackendLnetoV1, base); err != nil || !configured { t.Fatalf("ConfigureBackend = %v configured=%v", err, configured) } diff --git a/internal/binding/icmpv4/icmpv4.go b/internal/binding/icmpv4/icmpv4.go index 6fd3ddf..a27812f 100644 --- a/internal/binding/icmpv4/icmpv4.go +++ b/internal/binding/icmpv4/icmpv4.go @@ -22,7 +22,7 @@ const ( // Descriptor returns the opaque ICMPv4 module installed by the public facade. func Descriptor(backend ...plugin.Backend) plugin.Module { - return plugin.NewModule(plugin.ModuleICMPv4, func(registry *wago.Registry, host plugin.Host) { + return plugin.NewModule(plugin.ModuleICMPv4, func(registry *plugin.Registrar, host plugin.Host) { registry.Capability(Capability, wago.CapabilityDocs("use checked bounded ICMPv4 echo exchanges for the exact calling instance")) plugin.RegisterBindings(registry.ImportModule(Module), Bindings(host)) }, backend...) diff --git a/internal/binding/icmpv6/descriptor_test.go b/internal/binding/icmpv6/descriptor_test.go index fd7805c..ec2e051 100644 --- a/internal/binding/icmpv6/descriptor_test.go +++ b/internal/binding/icmpv6/descriptor_test.go @@ -6,7 +6,6 @@ import ( "github.com/wago-org/net/internal/guest" nscore "github.com/wago-org/net/internal/namespace/core" "github.com/wago-org/net/internal/plugin" - wago "github.com/wago-org/wago" ) func TestDescriptorInstallsCompleteBindingsAndPreservesBackend(t *testing.T) { @@ -26,7 +25,6 @@ func TestDescriptorInstallsCompleteBindingsAndPreservesBackend(t *testing.T) { }, ) descriptor := Descriptor(backend) - descriptor.Install(new(wago.Registry), plugin.Host{}) if err := descriptor.ConfigureBackend(plugin.BackendLnetoV1, base); err != nil || !configured { t.Fatalf("ConfigureBackend = %v configured=%v", err, configured) } diff --git a/internal/binding/icmpv6/icmpv6.go b/internal/binding/icmpv6/icmpv6.go index ee66683..73a6eb6 100644 --- a/internal/binding/icmpv6/icmpv6.go +++ b/internal/binding/icmpv6/icmpv6.go @@ -19,7 +19,7 @@ const ( ) func Descriptor(backend ...plugin.Backend) plugin.Module { - return plugin.NewModule(plugin.ModuleICMPv6, func(registry *wago.Registry, host plugin.Host) { + return plugin.NewModule(plugin.ModuleICMPv6, func(registry *plugin.Registrar, host plugin.Host) { registry.Capability(Capability, wago.CapabilityDocs("use checked bounded ICMPv6 echo and Neighbor Discovery for the exact calling instance")) plugin.RegisterBindings(registry.ImportModule(Module), Bindings(host)) }, backend...) diff --git a/internal/binding/ipv6/descriptor_test.go b/internal/binding/ipv6/descriptor_test.go index 1e079a6..fe7e907 100644 --- a/internal/binding/ipv6/descriptor_test.go +++ b/internal/binding/ipv6/descriptor_test.go @@ -6,7 +6,6 @@ import ( nscore "github.com/wago-org/net/internal/namespace/core" "github.com/wago-org/net/internal/plugin" - wago "github.com/wago-org/wago" ) func TestDescriptorInstallsCompleteBindingsAndPreservesBackend(t *testing.T) { @@ -23,7 +22,6 @@ func TestDescriptorInstallsCompleteBindingsAndPreservesBackend(t *testing.T) { }, ) descriptor := Descriptor(backend) - descriptor.Install(new(wago.Registry), plugin.Host{}) if err := descriptor.ConfigureBackend(plugin.BackendLnetoV1, base); err != nil || !configured { t.Fatalf("ConfigureBackend = %v configured=%v", err, configured) } diff --git a/internal/binding/ipv6/ipv6.go b/internal/binding/ipv6/ipv6.go index c9a109b..2e88cdd 100644 --- a/internal/binding/ipv6/ipv6.go +++ b/internal/binding/ipv6/ipv6.go @@ -18,7 +18,7 @@ const ( ) func Descriptor(backend ...plugin.Backend) plugin.Module { - return plugin.NewModule(plugin.ModuleIPv6, func(registry *wago.Registry, host plugin.Host) { + return plugin.NewModule(plugin.ModuleIPv6, func(registry *plugin.Registrar, host plugin.Host) { registry.Capability(Capability, wago.CapabilityDocs("inspect and service one bounded configured IPv6 namespace")) plugin.RegisterBindings(registry.ImportModule(Module), Bindings(host)) }, backend...) diff --git a/internal/binding/linklocal4/descriptor_test.go b/internal/binding/linklocal4/descriptor_test.go index 988981b..0d8ac0d 100644 --- a/internal/binding/linklocal4/descriptor_test.go +++ b/internal/binding/linklocal4/descriptor_test.go @@ -6,7 +6,6 @@ import ( nscore "github.com/wago-org/net/internal/namespace/core" "github.com/wago-org/net/internal/plugin" - wago "github.com/wago-org/wago" ) func TestDescriptorInstallsCompleteBindingsAndPreservesBackend(t *testing.T) { @@ -23,7 +22,6 @@ func TestDescriptorInstallsCompleteBindingsAndPreservesBackend(t *testing.T) { }, ) descriptor := Descriptor(backend) - descriptor.Install(new(wago.Registry), plugin.Host{}) if err := descriptor.ConfigureBackend(plugin.BackendLnetoV1, base); err != nil || !configured { t.Fatalf("ConfigureBackend = %v configured=%v", err, configured) } diff --git a/internal/binding/linklocal4/linklocal4.go b/internal/binding/linklocal4/linklocal4.go index 4bf7a16..5b7f10c 100644 --- a/internal/binding/linklocal4/linklocal4.go +++ b/internal/binding/linklocal4/linklocal4.go @@ -19,7 +19,7 @@ const ( ) func Descriptor(backend ...plugin.Backend) plugin.Module { - return plugin.NewModule(plugin.ModuleLinkLocal4, func(registry *wago.Registry, host plugin.Host) { + return plugin.NewModule(plugin.ModuleLinkLocal4, func(registry *plugin.Registrar, host plugin.Host) { registry.Capability(Capability, wago.CapabilityDocs("use bounded IPv4 link-local/APIPA claim-and-defend operations")) plugin.RegisterBindings(registry.ImportModule(Module), Bindings(host)) }, backend...) diff --git a/internal/binding/mdns/descriptor_test.go b/internal/binding/mdns/descriptor_test.go index 4ee5650..86b8785 100644 --- a/internal/binding/mdns/descriptor_test.go +++ b/internal/binding/mdns/descriptor_test.go @@ -6,7 +6,6 @@ import ( nscore "github.com/wago-org/net/internal/namespace/core" "github.com/wago-org/net/internal/plugin" - wago "github.com/wago-org/wago" ) func TestDescriptorInstallsCompleteBindingsAndPreservesBackend(t *testing.T) { @@ -23,7 +22,6 @@ func TestDescriptorInstallsCompleteBindingsAndPreservesBackend(t *testing.T) { }, ) descriptor := Descriptor(backend) - descriptor.Install(new(wago.Registry), plugin.Host{}) if err := descriptor.ConfigureBackend(plugin.BackendLnetoV1, base); err != nil || !configured { t.Fatalf("ConfigureBackend = %v configured=%v", err, configured) } diff --git a/internal/binding/mdns/mdns.go b/internal/binding/mdns/mdns.go index 84b6d0c..ddbd75c 100644 --- a/internal/binding/mdns/mdns.go +++ b/internal/binding/mdns/mdns.go @@ -19,7 +19,7 @@ const ( ) func Descriptor(backend ...plugin.Backend) plugin.Module { - return plugin.NewModule(plugin.ModuleMDNS, func(registry *wago.Registry, host plugin.Host) { + return plugin.NewModule(plugin.ModuleMDNS, func(registry *plugin.Registrar, host plugin.Host) { registry.Capability(Capability, wago.CapabilityDocs("use checked bounded multicast DNS queries, configured responses, and announcements")) plugin.RegisterBindings(registry.ImportModule(Module), Bindings(host)) }, backend...) diff --git a/internal/binding/ntp/descriptor_test.go b/internal/binding/ntp/descriptor_test.go index 0929d58..d40a14e 100644 --- a/internal/binding/ntp/descriptor_test.go +++ b/internal/binding/ntp/descriptor_test.go @@ -7,7 +7,6 @@ import ( "github.com/wago-org/net/internal/guest" nscore "github.com/wago-org/net/internal/namespace/core" "github.com/wago-org/net/internal/plugin" - wago "github.com/wago-org/wago" ) func TestDescriptorInstallsCompleteBindingsAndPreservesBackend(t *testing.T) { @@ -24,7 +23,6 @@ func TestDescriptorInstallsCompleteBindingsAndPreservesBackend(t *testing.T) { }, ) descriptor := Descriptor(backend) - descriptor.Install(new(wago.Registry), plugin.Host{}) if err := descriptor.ConfigureBackend(plugin.BackendLnetoV1, base); err != nil || !configured { t.Fatalf("ConfigureBackend = %v configured=%v", err, configured) } diff --git a/internal/binding/ntp/ntp.go b/internal/binding/ntp/ntp.go index 11fc10f..b85d944 100644 --- a/internal/binding/ntp/ntp.go +++ b/internal/binding/ntp/ntp.go @@ -22,7 +22,7 @@ const ( // Descriptor returns the opaque NTP module installed by the public facade. func Descriptor(backend ...plugin.Backend) plugin.Module { - return plugin.NewModule(plugin.ModuleNTP, func(registry *wago.Registry, host plugin.Host) { + return plugin.NewModule(plugin.ModuleNTP, func(registry *plugin.Registrar, host plugin.Host) { registry.Capability(Capability, wago.CapabilityDocs("use checked bounded NTP synchronizations with explicit host clock authority")) plugin.RegisterBindings(registry.ImportModule(Module), Bindings(host)) }, backend...) diff --git a/internal/binding/tcp/descriptor_test.go b/internal/binding/tcp/descriptor_test.go index 4e08a10..afb3792 100644 --- a/internal/binding/tcp/descriptor_test.go +++ b/internal/binding/tcp/descriptor_test.go @@ -6,7 +6,6 @@ import ( nscore "github.com/wago-org/net/internal/namespace/core" "github.com/wago-org/net/internal/plugin" - wago "github.com/wago-org/wago" ) func TestDescriptorInstallsCompleteBindingsAndPreservesBackend(t *testing.T) { @@ -23,7 +22,6 @@ func TestDescriptorInstallsCompleteBindingsAndPreservesBackend(t *testing.T) { }, ) descriptor := Descriptor(backend) - descriptor.Install(new(wago.Registry), plugin.Host{}) if err := descriptor.ConfigureBackend(plugin.BackendLnetoV1, base); err != nil || !configured { t.Fatalf("ConfigureBackend = %v configured=%v", err, configured) } diff --git a/internal/binding/tcp/tcp.go b/internal/binding/tcp/tcp.go index 3ce0111..667e30b 100644 --- a/internal/binding/tcp/tcp.go +++ b/internal/binding/tcp/tcp.go @@ -22,7 +22,7 @@ const ( // Descriptor returns the opaque TCP module installed by the public tcp facade // and the bounded aggregate compatibility path. func Descriptor(backend ...plugin.Backend) plugin.Module { - return plugin.NewModule(plugin.ModuleTCP, func(registry *wago.Registry, host plugin.Host) { + return plugin.NewModule(plugin.ModuleTCP, func(registry *plugin.Registrar, host plugin.Host) { registry.Capability(Capability, wago.CapabilityDocs("use checked nonblocking TCP networking for the exact calling instance")) plugin.RegisterBindings(registry.ImportModule(Module), Bindings(host)) }, backend...) diff --git a/internal/binding/tls/descriptor_test.go b/internal/binding/tls/descriptor_test.go index 8120a60..bd6d94f 100644 --- a/internal/binding/tls/descriptor_test.go +++ b/internal/binding/tls/descriptor_test.go @@ -6,7 +6,6 @@ import ( nscore "github.com/wago-org/net/internal/namespace/core" "github.com/wago-org/net/internal/plugin" - wago "github.com/wago-org/wago" ) func TestDescriptorInstallsExactTLSBindingsAndPreservesBackend(t *testing.T) { @@ -23,7 +22,6 @@ func TestDescriptorInstallsExactTLSBindingsAndPreservesBackend(t *testing.T) { }, ) descriptor := Descriptor(backend) - descriptor.Install(new(wago.Registry), plugin.Host{}) if err := descriptor.ConfigureBackend(plugin.BackendLnetoV1, base); err != nil || !configured { t.Fatalf("ConfigureBackend = %v configured=%v", err, configured) } diff --git a/internal/binding/tls/tls.go b/internal/binding/tls/tls.go index 2116002..7fb991a 100644 --- a/internal/binding/tls/tls.go +++ b/internal/binding/tls/tls.go @@ -22,7 +22,7 @@ const ( ) func Descriptor(backend ...plugin.Backend) plugin.Module { - return plugin.NewModule(plugin.ModuleTLS, func(registry *wago.Registry, host plugin.Host) { + return plugin.NewModule(plugin.ModuleTLS, func(registry *plugin.Registrar, host plugin.Host) { registry.Capability(Capability, wago.CapabilityDocs("use checked outbound verified TLS client streams for the exact calling instance")) plugin.RegisterBindings(registry.ImportModule(Module), Bindings(host)) }, backend...) diff --git a/internal/binding/udp/descriptor_test.go b/internal/binding/udp/descriptor_test.go index 0272481..db0ac5b 100644 --- a/internal/binding/udp/descriptor_test.go +++ b/internal/binding/udp/descriptor_test.go @@ -6,7 +6,6 @@ import ( nscore "github.com/wago-org/net/internal/namespace/core" "github.com/wago-org/net/internal/plugin" - wago "github.com/wago-org/wago" ) func TestDescriptorInstallsCompleteBindingsAndPreservesBackend(t *testing.T) { @@ -23,7 +22,6 @@ func TestDescriptorInstallsCompleteBindingsAndPreservesBackend(t *testing.T) { }, ) descriptor := Descriptor(backend) - descriptor.Install(new(wago.Registry), plugin.Host{}) if err := descriptor.ConfigureBackend(plugin.BackendLnetoV1, base); err != nil || !configured { t.Fatalf("ConfigureBackend = %v configured=%v", err, configured) } diff --git a/internal/binding/udp/udp.go b/internal/binding/udp/udp.go index 3a2f9ab..374b9c9 100644 --- a/internal/binding/udp/udp.go +++ b/internal/binding/udp/udp.go @@ -22,7 +22,7 @@ const ( // Descriptor returns the opaque UDP module installed by the public udp facade // and the bounded aggregate compatibility path. func Descriptor(backend ...plugin.Backend) plugin.Module { - return plugin.NewModule(plugin.ModuleUDP, func(registry *wago.Registry, host plugin.Host) { + return plugin.NewModule(plugin.ModuleUDP, func(registry *plugin.Registrar, host plugin.Host) { registry.Capability(Capability, wago.CapabilityDocs("use checked nonblocking UDP networking for the exact calling instance")) plugin.RegisterBindings(registry.ImportModule(Module), Bindings(host)) }, backend...) diff --git a/internal/dependencytest/dependencies_test.go b/internal/dependencytest/dependencies_test.go index 88bddc5..69eed70 100644 --- a/internal/dependencytest/dependencies_test.go +++ b/internal/dependencytest/dependencies_test.go @@ -140,7 +140,7 @@ func TestSelfRegisterPackageDependencyBoundaries(t *testing.T) { for protocol, dependency := range protocolDependencies { if test.selected[protocol] { if !dependencies[dependency.public] || !dependencies[dependency.binding] || !dependencies[dependency.operation] || !dependencies[dependency.abi] || !dependencies[dependency.namespace] || !dependencies[dependency.adapter] { - t.Fatalf("selected %s self-register graph is incomplete", protocol) + t.Fatalf("selected %s provider graph is incomplete", protocol) } if test.name != "all" && dependency.register != "" && !dependencies[dependency.register] { t.Fatalf("selected %s granular register package absent", protocol) @@ -149,19 +149,19 @@ func TestSelfRegisterPackageDependencyBoundaries(t *testing.T) { } if protocol == "tcp" && test.selected["tls"] { if dependencies[dependency.public] || dependencies[dependency.register] || dependencies[dependency.binding] || dependencies[dependency.operation] || dependencies[dependency.abi] { - t.Fatal("TLS self-register graph reached raw TCP facade") + t.Fatal("TLS composition graph reached raw TCP facade") } if !dependencies[dependency.namespace] || !dependencies[dependency.adapter] { - t.Fatal("TLS self-register graph omitted private TCP transport") + t.Fatal("TLS composition graph omitted private TCP transport") } continue } if dependencies[dependency.public] || dependencies[dependency.register] || dependencies[dependency.binding] || dependencies[dependency.operation] || dependencies[dependency.abi] || dependencies[dependency.namespace] || dependencies[dependency.adapter] { - t.Fatalf("unselected %s compiled by %s self-register graph", protocol, test.name) + t.Fatalf("unselected %s compiled by %s provider graph", protocol, test.name) } } if dependencies[modulePath+"/compat"] || dependencies[modulePath+"/internal/namespace"] || dependencies[modulePath+"/internal/backend/lneto"] { - t.Fatalf("%s self-register graph reached an aggregate compatibility implementation", test.name) + t.Fatalf("%s provider graph reached an aggregate compatibility implementation", test.name) } if test.name == "all" && !dependencies[modulePath+"/register"] { t.Fatal("all-protocol register bundle absent") diff --git a/internal/dependencytest/inspection_test.go b/internal/dependencytest/inspection_test.go index e35a77c..551c15f 100644 --- a/internal/dependencytest/inspection_test.go +++ b/internal/dependencytest/inspection_test.go @@ -23,6 +23,7 @@ import ( tlsfixture "github.com/wago-org/net/internal/dependencytest/testdata/tls" udpfixture "github.com/wago-org/net/internal/dependencytest/testdata/udp" udpdnsfixture "github.com/wago-org/net/internal/dependencytest/testdata/udpdns" + "github.com/wago-org/net/internal/plugintest" wago "github.com/wago-org/wago" ) @@ -62,8 +63,11 @@ func TestFixtureRuntimeInspection(t *testing.T) { t.Fatalf("compose fixture: %v", err) } runtime := wago.NewRuntime() - if err := runtime.Use(network); err != nil { - t.Fatalf("Use: %v", err) + defer runtime.Close() + if len(test.imports) != 0 { + if err := plugintest.LoadNetwork(runtime, network); err != nil { + t.Fatalf("LoadPlugins: %v", err) + } } if got := runtime.Capabilities(); !reflect.DeepEqual(got, test.capabilities) { t.Fatalf("Capabilities = %v, want %v", got, test.capabilities) diff --git a/internal/inspectionpolicy/policy.go b/internal/inspectionpolicy/policy.go index 500a03f..7438ecb 100644 --- a/internal/inspectionpolicy/policy.go +++ b/internal/inspectionpolicy/policy.go @@ -9,7 +9,7 @@ import ( "sort" ) -const AggregateKey = "net" +const AggregateKey = "github.com/wago-org/net" //go:embed policy.json var policyJSON []byte diff --git a/internal/inspectionpolicy/policy.json b/internal/inspectionpolicy/policy.json index 263a8e4..98f2fee 100644 --- a/internal/inspectionpolicy/policy.json +++ b/internal/inspectionpolicy/policy.json @@ -1,73 +1,73 @@ { "bundles": [ { - "key": "net", + "key": "github.com/wago-org/net", "package": "github.com/wago-org/net/register", "capabilities": ["net.dhcpv4", "net.dhcpv6", "net.dns", "net.icmpv4", "net.icmpv6", "net.info", "net.ipv6", "net.linklocal4", "net.mdns", "net.ntp", "net.tcp", "net.udp"], "imports": {"wago_net": 1, "wago_net_dhcpv4": 7, "wago_net_dhcpv6": 7, "wago_net_dns": 6, "wago_net_icmpv4": 6, "wago_net_icmpv6": 14, "wago_net_ipv6": 3, "wago_net_linklocal4": 7, "wago_net_mdns": 10, "wago_net_ntp": 6, "wago_net_tcp": 11, "wago_net_udp": 6} }, { - "key": "net-dhcpv4", + "key": "github.com/wago-org/net/dhcpv4", "package": "github.com/wago-org/net/dhcpv4/register", "capabilities": ["net.dhcpv4", "net.info"], "imports": {"wago_net": 1, "wago_net_dhcpv4": 7} }, { - "key": "net-dhcpv6", + "key": "github.com/wago-org/net/dhcpv6", "package": "github.com/wago-org/net/dhcpv6/register", "capabilities": ["net.dhcpv6", "net.info"], "imports": {"wago_net": 1, "wago_net_dhcpv6": 7} }, { - "key": "net-dns", + "key": "github.com/wago-org/net/dns", "package": "github.com/wago-org/net/dns/register", "capabilities": ["net.dns", "net.info"], "imports": {"wago_net": 1, "wago_net_dns": 6} }, { - "key": "net-icmpv4", + "key": "github.com/wago-org/net/icmpv4", "package": "github.com/wago-org/net/icmpv4/register", "capabilities": ["net.icmpv4", "net.info"], "imports": {"wago_net": 1, "wago_net_icmpv4": 6} }, { - "key": "net-icmpv6", + "key": "github.com/wago-org/net/icmpv6", "package": "github.com/wago-org/net/icmpv6/register", "capabilities": ["net.icmpv6", "net.info"], "imports": {"wago_net": 1, "wago_net_icmpv6": 14} }, { - "key": "net-ipv6", + "key": "github.com/wago-org/net/ipv6", "package": "github.com/wago-org/net/ipv6/register", "capabilities": ["net.info", "net.ipv6"], "imports": {"wago_net": 1, "wago_net_ipv6": 3} }, { - "key": "net-linklocal4", + "key": "github.com/wago-org/net/linklocal4", "package": "github.com/wago-org/net/linklocal4/register", "capabilities": ["net.info", "net.linklocal4"], "imports": {"wago_net": 1, "wago_net_linklocal4": 7} }, { - "key": "net-mdns", + "key": "github.com/wago-org/net/mdns", "package": "github.com/wago-org/net/mdns/register", "capabilities": ["net.info", "net.mdns"], "imports": {"wago_net": 1, "wago_net_mdns": 10} }, { - "key": "net-ntp", + "key": "github.com/wago-org/net/ntp", "package": "github.com/wago-org/net/ntp/register", "capabilities": ["net.info", "net.ntp"], "imports": {"wago_net": 1, "wago_net_ntp": 6} }, { - "key": "net-tcp", + "key": "github.com/wago-org/net/tcp", "package": "github.com/wago-org/net/tcp/register", "capabilities": ["net.info", "net.tcp"], "imports": {"wago_net": 1, "wago_net_tcp": 11} }, { - "key": "net-udp", + "key": "github.com/wago-org/net/udp", "package": "github.com/wago-org/net/udp/register", "capabilities": ["net.info", "net.udp"], "imports": {"wago_net": 1, "wago_net_udp": 6} diff --git a/internal/inspectionpolicy/policy_test.go b/internal/inspectionpolicy/policy_test.go index 4912d5f..a10e05a 100644 --- a/internal/inspectionpolicy/policy_test.go +++ b/internal/inspectionpolicy/policy_test.go @@ -1,28 +1,49 @@ package inspectionpolicy_test import ( + "context" + "encoding/json" + "os" "path/filepath" "reflect" "sort" "strings" "testing" - _ "github.com/wago-org/net/dhcpv4/register" - _ "github.com/wago-org/net/dhcpv6/register" - _ "github.com/wago-org/net/dns/register" - _ "github.com/wago-org/net/icmpv4/register" - _ "github.com/wago-org/net/icmpv6/register" "github.com/wago-org/net/internal/inspectionpolicy" - _ "github.com/wago-org/net/ipv6/register" - _ "github.com/wago-org/net/linklocal4/register" - _ "github.com/wago-org/net/mdns/register" - _ "github.com/wago-org/net/ntp/register" - _ "github.com/wago-org/net/register" - _ "github.com/wago-org/net/tcp/register" - _ "github.com/wago-org/net/udp/register" + "github.com/wago-org/net/internal/plugintest" + allregister "github.com/wago-org/net/register" wago "github.com/wago-org/wago" ) +func catalogProviders() map[string]wago.PluginProvider { + providers := allregister.Providers() + catalog := make(map[string]wago.PluginProvider, len(providers)) + for _, provider := range providers { + catalog[provider.Definition.ID] = provider + } + return catalog +} + +type manifestAuthor struct { + Name string `json:"name"` +} + +type manifestPackage 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"` + Homepage string `json:"homepage"` + Repository string `json:"repository"` + Authors []manifestAuthor `json:"authors"` + Engines map[string]string `json:"engines"` + Platforms []string `json:"platforms"` + Subpackages []manifestPackage `json:"subpackages"` +} + func TestCanonicalPolicyMatchesEveryRegisteredBundle(t *testing.T) { policy, err := inspectionpolicy.Load() if err != nil { @@ -31,17 +52,19 @@ func TestCanonicalPolicyMatchesEveryRegisteredBundle(t *testing.T) { if len(policy.Bundles) != 12 { t.Fatalf("bundle count = %d, want 12", len(policy.Bundles)) } + providers := catalogProviders() for _, bundle := range policy.Bundles { - extension, ok := wago.NewExtension(bundle.Key) + provider, ok := providers[bundle.Key] if !ok { - t.Errorf("registered extension %q is missing", bundle.Key) + t.Errorf("explicit provider %q is missing", bundle.Key) continue } runtime := wago.NewRuntime() - if err := runtime.Use(extension); err != nil { - t.Errorf("Use %q: %v", bundle.Key, err) + if err := runtime.LoadPlugins(context.Background(), plugintest.Set(provider)); err != nil { + t.Errorf("LoadPlugins %q: %v", bundle.Key, err) continue } + defer runtime.Close() capabilities := make([]string, len(runtime.Capabilities())) for index, capability := range runtime.Capabilities() { capabilities[index] = string(capability) @@ -62,6 +85,154 @@ func TestCanonicalPolicyMatchesEveryRegisteredBundle(t *testing.T) { } } +func TestV1ManifestMatchesEveryExplicitProviderDefinition(t *testing.T) { + data, err := os.ReadFile(filepath.Join("..", "..", "wago.json")) + if err != nil { + t.Fatal(err) + } + var manifest struct { + Schema string `json:"$schema"` + Package manifestPackage `json:"package"` + Plugins map[string]string `json:"plugins"` + } + if err := json.Unmarshal(data, &manifest); err != nil { + t.Fatal(err) + } + if manifest.Schema != "https://wago.sh/v1/schema.json" { + t.Fatalf("manifest schema = %q", manifest.Schema) + } + if len(manifest.Plugins) != 0 { + t.Fatalf("leaf manifest dependencies = %v", manifest.Plugins) + } + metadata := map[string]manifestPackage{manifest.Package.Module: manifest.Package} + for _, subpackage := range manifest.Package.Subpackages { + if _, duplicate := metadata[subpackage.Module]; duplicate { + t.Fatalf("duplicate manifest provider %q", subpackage.Module) + } + metadata[subpackage.Module] = inheritManifestMetadata(manifest.Package, subpackage) + } + providers := catalogProviders() + if len(metadata) != len(providers) { + t.Fatalf("manifest entries = %d, catalog providers = %d", len(metadata), len(providers)) + } + for id, provider := range providers { + entry, ok := metadata[id] + if !ok { + t.Fatalf("manifest omits catalog provider %q", id) + } + assertManifestMetadata(t, entry, provider.Definition) + } + catalog := allregister.Providers() + want, err := wago.EncodeProviderCatalog("github.com/wago-org/net/register", catalog) + if err != nil { + t.Fatal(err) + } + got, err := os.ReadFile(filepath.Join("..", "..", wago.ProviderCatalogFile)) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, want) { + t.Fatalf("%s is stale; run wago plugin catalog", wago.ProviderCatalogFile) + } + document, err := wago.DecodeProviderCatalog(got) + if err != nil { + t.Fatalf("%s: %v", wago.ProviderCatalogFile, err) + } + if len(document.Providers) != len(providers) { + t.Fatalf("artifact providers = %d, want %d", len(document.Providers), len(providers)) + } +} + +func inheritManifestMetadata(root, subpackage manifestPackage) manifestPackage { + if subpackage.Version == "" { + subpackage.Version = root.Version + } + if subpackage.License == "" { + subpackage.License = root.License + } + if subpackage.Homepage == "" { + subpackage.Homepage = root.Homepage + } + if subpackage.Repository == "" { + subpackage.Repository = root.Repository + } + if subpackage.Authors == nil { + subpackage.Authors = root.Authors + } + if subpackage.Engines == nil { + subpackage.Engines = root.Engines + } + if subpackage.Platforms == nil { + subpackage.Platforms = root.Platforms + } + return subpackage +} + +func assertManifestMetadata(t *testing.T, manifest manifestPackage, definition wago.PluginDefinition) { + t.Helper() + authors := make([]string, len(manifest.Authors)) + for i := range manifest.Authors { + authors[i] = manifest.Authors[i].Name + } + if manifest.Module != definition.ID || + manifest.Version != definition.Version || + manifest.Name != definition.Name || + manifest.Description != definition.Description || + manifest.Stability != definition.Stability || + manifest.License != definition.Provenance.License || + manifest.Homepage != definition.Provenance.Homepage || + manifest.Repository != definition.Provenance.Repository || + !reflect.DeepEqual(authors, definition.Provenance.Authors) || + !reflect.DeepEqual(manifest.Engines, definition.Compatibility.Engines) || + !reflect.DeepEqual(manifest.Platforms, definition.Compatibility.Platforms) { + t.Fatalf("%s manifest metadata drifted\nmanifest=%#v\ndefinition=%#v", definition.ID, manifest, definition) + } +} + +func TestCatalogDefinitionsRequestExactAuthoritiesAndContract(t *testing.T) { + policy, err := inspectionpolicy.Load() + if err != nil { + t.Fatal(err) + } + providers := catalogProviders() + for _, bundle := range policy.Bundles { + provider := providers[bundle.Key] + definition := provider.Definition + if len(definition.Authorities) != 4 { + t.Fatalf("%s authorities = %d, want 4", bundle.Key, len(definition.Authorities)) + } + wantModules := make([]string, 0, len(bundle.Imports)) + for module := range bundle.Imports { + wantModules = append(wantModules, module) + } + sort.Strings(wantModules) + wantAuthorities := []wago.Authority{ + wago.AuthorityHostImportDefine, + wago.AuthorityHostCallerIdentify, + wago.AuthorityInstanceInstantiateIntercept, + wago.AuthorityInstanceCloseObserve, + } + for i, authority := range definition.Authorities { + if authority.Name != wantAuthorities[i] || authority.Mode != wago.AuthorityRequired { + t.Fatalf("%s authority[%d] = %q/%q", bundle.Key, i, authority.Name, authority.Mode) + } + if authority.Name == wago.AuthorityHostImportDefine { + if !reflect.DeepEqual(authority.Scope.Modules, wantModules) { + t.Fatalf("%s import authority = %v, want %v", bundle.Key, authority.Scope.Modules, wantModules) + } + } else if !reflect.DeepEqual(authority.Scope, wago.AuthorityScope{}) { + t.Fatalf("%s authority %s has unexpected scope %+v", bundle.Key, authority.Name, authority.Scope) + } + } + if got, want := definition.Provides, []wago.ContractSpec{{ID: "github.com/wago-org/net/service", Major: 1}}; !reflect.DeepEqual(got, want) { + t.Fatalf("%s provides = %+v, want %+v", bundle.Key, got, want) + } + if len(definition.Requires) != 0 || len(definition.Consumes) != 0 { + t.Fatalf("%s unexpected dependencies: requires=%v consumes=%v", bundle.Key, definition.Requires, definition.Consumes) + } + } +} + func TestEveryRegisterPackageHasCanonicalExpectation(t *testing.T) { policy, err := inspectionpolicy.Load() if err != nil { diff --git a/internal/instance/core/manager.go b/internal/instance/core/manager.go index a953cc7..f40412e 100644 --- a/internal/instance/core/manager.go +++ b/internal/instance/core/manager.go @@ -37,7 +37,7 @@ type Config struct { NamespaceFactory NamespaceFactory } -// DefaultConfig preserves the core extension's state-only behavior: finite +// DefaultConfig preserves the core provider's state-only behavior: finite // default quotas and readiness storage with no automatically created namespace. func DefaultConfig() Config { return Config{Limits: quota.DefaultLimits(), Readiness: readiness.DefaultConfig()} @@ -94,7 +94,7 @@ func (s *State) Policy() *policy.Policy { } // NamespaceHandle returns the automatically created namespace handle, or zero -// when the extension was configured without a namespace. +// when the network was configured without a namespace. func (s *State) NamespaceHandle() resource.Handle { if s == nil { return 0 @@ -173,13 +173,13 @@ func runTeardownStep(step func() error) (err error, panicValue any, panicked boo return err, nil, false } -// Manager is an extension-local attachment map. It must be owned by an -// extension value; it is intentionally not a package-global registry. +// Manager is a provider-local attachment map. It must be owned by one Network; +// it is intentionally not a package-global registry. type Manager struct { mu sync.RWMutex - states map[*wago.Instance]*State - attaching map[*wago.Instance]*attachmentAttempt - detaching map[*wago.Instance]*detachmentAttempt + states map[instanceKey]*State + attaching map[instanceKey]*attachmentAttempt + detaching map[instanceKey]*detachmentAttempt policy *policy.Policy limits quota.Limits @@ -187,6 +187,13 @@ type Manager struct { namespaceFactory NamespaceFactory } +// instanceKey keeps the production opaque-identity path distinct from the +// direct-pointer path used by low-level repository fixtures. +type instanceKey struct { + direct *wago.Instance + identity wago.InstanceIdentity +} + type attachmentAttempt struct { done chan struct{} } @@ -196,7 +203,7 @@ type detachmentAttempt struct { err error } -// NewManager creates an empty extension-local manager with finite defaults and +// NewManager creates an empty provider-local manager with finite defaults and // no automatically created namespace. func NewManager() *Manager { manager, err := NewManagerConfigured(DefaultConfig()) @@ -218,9 +225,9 @@ func NewManagerConfigured(config Config) (*Manager, error) { return nil, fmt.Errorf("compile endpoint policy: %w", err) } return &Manager{ - states: make(map[*wago.Instance]*State), - attaching: make(map[*wago.Instance]*attachmentAttempt), - detaching: make(map[*wago.Instance]*detachmentAttempt), + states: make(map[instanceKey]*State), + attaching: make(map[instanceKey]*attachmentAttempt), + detaching: make(map[instanceKey]*detachmentAttempt), policy: compiled, limits: config.Limits, readiness: config.Readiness, @@ -238,22 +245,6 @@ func rightSizeReadiness(configured int, resources uint64) int { return configured } -// AfterInstantiate is a Wago lifecycle hook that attaches fresh state after a -// Runtime instance has been created. -func (m *Manager) AfterInstantiate(_ *wago.InstantiateContext, instance *wago.Instance) error { - return m.Attach(instance) -} - -// BeforeClose is a Wago lifecycle hook that removes state before instance -// memory and runtime resources are invalidated. Wago close hooks cannot return -// errors, so all resources are attempted and cleanup errors are contained. -func (m *Manager) BeforeClose(context *wago.InstanceContext) { - if context == nil { - return - } - _ = m.Detach(context.Instance) -} - // Attach creates and publishes one isolated state for instance. An optional // namespace is fully quota-owned, generation-safe, and poll-registered before // publication; every failed stage is rolled back synchronously. @@ -261,13 +252,43 @@ func (m *Manager) Attach(instance *wago.Instance) error { if m == nil || instance == nil { return ErrInvalidInstance } - attempt, err := m.beginAttachment(instance) + return m.attach(instanceKey{direct: instance}) +} + +// AttachIdentity creates and publishes state for an opaque Runtime identity. +func (m *Manager) AttachIdentity(identity wago.InstanceIdentity) error { + if m == nil || identity.IsZero() { + return ErrInvalidInstance + } + return m.attach(instanceKey{identity: identity}) +} + +// SingleState returns the attached state only when the manager owns exactly +// one instance. It supports repository diagnostics without weakening the +// production caller-identity boundary. +func (m *Manager) SingleState() (*State, bool) { + if m == nil { + return nil, false + } + m.mu.RLock() + defer m.mu.RUnlock() + if len(m.states) != 1 { + return nil, false + } + for _, state := range m.states { + return state, true + } + return nil, false +} + +func (m *Manager) attach(key instanceKey) error { + attempt, err := m.beginAttachment(key) if err != nil { return err } result := attachmentResult{} - m.runAttachment(instance, &result) - m.completeAttachment(instance, attempt, result.state, result.published, result.panicValue, result.panicked) + m.runAttachment(key, &result) + m.completeAttachment(key, attempt, result.state, result.published, result.panicValue, result.panicked) return result.err } @@ -283,7 +304,7 @@ type attachmentResult struct { // can finish before Attach re-panics from an ordinary call frame. In particular, // this avoids relying on a recover-and-repanic cycle inside the same deferred // function, which is not portable across all supported Go toolchains. -func (m *Manager) runAttachment(instance *wago.Instance, result *attachmentResult) { +func (m *Manager) runAttachment(key instanceKey, result *attachmentResult) { completed := false defer func() { if !completed { @@ -291,11 +312,11 @@ func (m *Manager) runAttachment(instance *wago.Instance, result *attachmentResul result.panicked = true } }() - result.err = m.attachState(instance, &result.state, &result.published) + result.err = m.attachState(key, &result.state, &result.published) completed = true } -func (m *Manager) attachState(instance *wago.Instance, state **State, published *bool) error { +func (m *Manager) attachState(key instanceKey, state **State, published *bool) error { table, err := resource.NewTable() if err != nil { return fmt.Errorf("create resource table: %w", err) @@ -317,36 +338,36 @@ func (m *Manager) attachState(instance *wago.Instance, state **State, published m.mu.Lock() if m.states == nil { - m.states = make(map[*wago.Instance]*State) + m.states = make(map[instanceKey]*State) } - m.states[instance] = *state + m.states[key] = *state *published = true m.mu.Unlock() return nil } -func (m *Manager) beginAttachment(instance *wago.Instance) (*attachmentAttempt, error) { +func (m *Manager) beginAttachment(key instanceKey) (*attachmentAttempt, error) { for { m.mu.Lock() - if _, exists := m.states[instance]; exists { + if _, exists := m.states[key]; exists { m.mu.Unlock() return nil, ErrAlreadyAttached } - if _, exists := m.attaching[instance]; exists { + if _, exists := m.attaching[key]; exists { m.mu.Unlock() return nil, ErrAlreadyAttached } - if attempt := m.detaching[instance]; attempt != nil { + if attempt := m.detaching[key]; attempt != nil { done := attempt.done m.mu.Unlock() <-done continue } if m.attaching == nil { - m.attaching = make(map[*wago.Instance]*attachmentAttempt) + m.attaching = make(map[instanceKey]*attachmentAttempt) } attempt := &attachmentAttempt{done: make(chan struct{})} - m.attaching[instance] = attempt + m.attaching[key] = attempt m.mu.Unlock() return attempt, nil } @@ -355,12 +376,12 @@ func (m *Manager) beginAttachment(instance *wago.Instance) (*attachmentAttempt, // completeAttachment retires the lifecycle record before re-propagating a // panic. A construction panic takes precedence over a rollback-close panic; // when construction returned normally, the rollback panic remains visible. -func (m *Manager) completeAttachment(instance *wago.Instance, attempt *attachmentAttempt, state *State, published bool, originalPanic any, originalPanicked bool) { +func (m *Manager) completeAttachment(key instanceKey, attempt *attachmentAttempt, state *State, published bool, originalPanic any, originalPanicked bool) { var cleanupPanic any if !published && state != nil { cleanupPanic = closeUnpublishedState(state) } - m.finishAttachment(instance, attempt) + m.finishAttachment(key, attempt) if originalPanicked { panic(originalPanic) } @@ -381,10 +402,10 @@ func closeUnpublishedState(state *State) (panicValue any) { return nil } -func (m *Manager) finishAttachment(instance *wago.Instance, attempt *attachmentAttempt) { +func (m *Manager) finishAttachment(key instanceKey, attempt *attachmentAttempt) { m.mu.Lock() - if m.attaching[instance] == attempt { - delete(m.attaching, instance) + if m.attaching[key] == attempt { + delete(m.attaching, key) close(attempt.done) } m.mu.Unlock() @@ -588,45 +609,57 @@ func (m *Manager) Detach(instance *wago.Instance) error { if m == nil || instance == nil { return ErrInvalidInstance } + return m.detach(instanceKey{direct: instance}) +} + +// DetachIdentity unpublishes and closes state for an opaque Runtime identity. +func (m *Manager) DetachIdentity(identity wago.InstanceIdentity) error { + if m == nil || identity.IsZero() { + return ErrInvalidInstance + } + return m.detach(instanceKey{identity: identity}) +} + +func (m *Manager) detach(key instanceKey) error { for { m.mu.Lock() - if attempt := m.attaching[instance]; attempt != nil { + if attempt := m.attaching[key]; attempt != nil { done := attempt.done m.mu.Unlock() <-done continue } - if attempt := m.detaching[instance]; attempt != nil { + if attempt := m.detaching[key]; attempt != nil { done := attempt.done m.mu.Unlock() <-done return attempt.err } - state := m.states[instance] + state := m.states[key] if state == nil { m.mu.Unlock() return nil } - delete(m.states, instance) + delete(m.states, key) if m.detaching == nil { - m.detaching = make(map[*wago.Instance]*detachmentAttempt) + m.detaching = make(map[instanceKey]*detachmentAttempt) } attempt := &detachmentAttempt{done: make(chan struct{})} - m.detaching[instance] = attempt + m.detaching[key] = attempt m.mu.Unlock() - return m.closeDetachedState(instance, attempt, state) + return m.closeDetachedState(key, attempt, state) } } -func (m *Manager) closeDetachedState(instance *wago.Instance, attempt *detachmentAttempt, state *State) error { +func (m *Manager) closeDetachedState(key instanceKey, attempt *detachmentAttempt, state *State) error { result := detachmentResult{} runDetachedClose(state, &result) waiterErr := result.err if result.panicked { waiterErr = ErrTeardownPanicked } - m.finishDetachment(instance, attempt, waiterErr) + m.finishDetachment(key, attempt, waiterErr) if result.panicked { panic(result.panicValue) } @@ -651,11 +684,11 @@ func runDetachedClose(state *State, result *detachmentResult) { completed = true } -func (m *Manager) finishDetachment(instance *wago.Instance, attempt *detachmentAttempt, closeErr error) { +func (m *Manager) finishDetachment(key instanceKey, attempt *detachmentAttempt, closeErr error) { m.mu.Lock() - if m.detaching[instance] == attempt { + if m.detaching[key] == attempt { attempt.err = closeErr - delete(m.detaching, instance) + delete(m.detaching, key) close(attempt.done) } m.mu.Unlock() @@ -666,20 +699,32 @@ func (m *Manager) ForInstance(instance *wago.Instance) (*State, bool) { if m == nil || instance == nil { return nil, false } + return m.lookup(instanceKey{direct: instance}) +} + +// ForIdentity returns state only for the exact opaque Runtime identity. +func (m *Manager) ForIdentity(identity wago.InstanceIdentity) (*State, bool) { + if m == nil || identity.IsZero() { + return nil, false + } + return m.lookup(instanceKey{identity: identity}) +} + +func (m *Manager) lookup(key instanceKey) (*State, bool) { m.mu.RLock() - state, ok := m.states[instance] + state, ok := m.states[key] m.mu.RUnlock() return state, ok } -// FromHost resolves the exact calling instance through Wago's optional host -// module identity surface. HostModule-only mocks and low-level imports without -// Runtime lifecycle attachment fail closed. +// FromHost is a test-only bridge for host-module fixtures that expose a direct +// instance pointer. Production plugin calls resolve opaque identity through +// Wago's granted CallerResolver instead. func (m *Manager) FromHost(module wago.HostModule) (*State, bool) { if m == nil || resource.IsNil(module) { return nil, false } - identity, ok := module.(wago.InstanceHostModule) + identity, ok := module.(interface{ Instance() *wago.Instance }) if !ok || resource.IsNil(identity) { return nil, false } diff --git a/internal/instance/core/manager_test.go b/internal/instance/core/manager_test.go index a6935cd..c164c5a 100644 --- a/internal/instance/core/manager_test.go +++ b/internal/instance/core/manager_test.go @@ -191,7 +191,7 @@ type fakePollable struct{} func (fakePollable) Close() error { return nil } func (fakePollable) Readiness() namespace.Readiness { return namespace.ReadyReadable } -func TestLifecycleHooksAttachAndDetachExactInstance(t *testing.T) { +func TestAttachAndDetachExactInstance(t *testing.T) { backend := new(fakeNamespace) config := DefaultConfig() config.NamespaceFactory = func(*policy.Policy, *quota.Account) (nscore.Namespace, error) { @@ -202,24 +202,24 @@ func TestLifecycleHooksAttachAndDetachExactInstance(t *testing.T) { t.Fatal(err) } instance := new(wago.Instance) - if err := manager.AfterInstantiate(nil, instance); err != nil { + if err := manager.Attach(instance); err != nil { t.Fatal(err) } if _, ok := manager.ForInstance(instance); !ok { t.Fatal("lifecycle hook did not publish exact instance state") } - if err := manager.AfterInstantiate(nil, instance); !errors.Is(err, ErrAlreadyAttached) { - t.Fatalf("duplicate AfterInstantiate = %v", err) + if err := manager.Attach(instance); !errors.Is(err, ErrAlreadyAttached) { + t.Fatalf("duplicate Attach = %v", err) } - manager.BeforeClose(nil) - if _, ok := manager.ForInstance(instance); !ok { - t.Fatal("nil close context detached live instance") + if err := manager.Detach(instance); err != nil { + t.Fatal(err) } - manager.BeforeClose(&wago.InstanceContext{Instance: instance}) if _, ok := manager.ForInstance(instance); ok { - t.Fatal("BeforeClose retained exact instance state") + t.Fatal("Detach retained exact instance state") + } + if err := manager.Detach(instance); err != nil { + t.Fatal(err) } - manager.BeforeClose(&wago.InstanceContext{Instance: instance}) if backend.closed.Load() != 1 { t.Fatalf("backend close calls = %d, want 1", backend.closed.Load()) } @@ -558,7 +558,7 @@ func TestPanickingAttachmentRollsBackAndAllowsRetry(t *testing.T) { t.Fatal("panicking attachment published state") } manager.mu.RLock() - _, retained := manager.attaching[instance] + _, retained := manager.attaching[instanceKey{direct: instance}] manager.mu.RUnlock() if retained { t.Fatal("panicking attachment retained lifecycle record") @@ -582,12 +582,13 @@ func TestLifecycleAttemptCompletionIsIdempotent(t *testing.T) { manager := NewManager() instance := new(wago.Instance) - attachment, err := manager.beginAttachment(instance) + key := instanceKey{direct: instance} + attachment, err := manager.beginAttachment(key) if err != nil { t.Fatal(err) } - manager.finishAttachment(instance, attachment) - manager.finishAttachment(instance, attachment) + manager.finishAttachment(key, attachment) + manager.finishAttachment(key, attachment) select { case <-attachment.done: default: @@ -598,10 +599,10 @@ func TestLifecycleAttemptCompletionIsIdempotent(t *testing.T) { secondErr := errors.New("duplicate teardown result") detachment := &detachmentAttempt{done: make(chan struct{})} manager.mu.Lock() - manager.detaching[instance] = detachment + manager.detaching[key] = detachment manager.mu.Unlock() - manager.finishDetachment(instance, detachment, firstErr) - manager.finishDetachment(instance, detachment, secondErr) + manager.finishDetachment(key, detachment, firstErr) + manager.finishDetachment(key, detachment, secondErr) select { case <-detachment.done: default: @@ -615,7 +616,8 @@ func TestLifecycleAttemptCompletionIsIdempotent(t *testing.T) { func TestAttachmentCompletionRetiresRecordAcrossRollbackClosePanic(t *testing.T) { manager := NewManager() instance := new(wago.Instance) - attempt, err := manager.beginAttachment(instance) + key := instanceKey{direct: instance} + attempt, err := manager.beginAttachment(key) if err != nil { t.Fatal(err) } @@ -632,7 +634,7 @@ func TestAttachmentCompletionRetiresRecordAcrossRollbackClosePanic(t *testing.T) var recovered any func() { defer func() { recovered = recover() }() - manager.completeAttachment(instance, attempt, state, false, originalPanic, true) + manager.completeAttachment(key, attempt, state, false, originalPanic, true) }() if recovered != originalPanic { t.Fatalf("recovered panic = %v, want original %v", recovered, originalPanic) @@ -648,7 +650,7 @@ func TestAttachmentCompletionRetiresRecordAcrossRollbackClosePanic(t *testing.T) t.Fatalf("manager length = %d, want zero", manager.Len()) } manager.mu.RLock() - _, retained := manager.attaching[instance] + _, retained := manager.attaching[key] manager.mu.RUnlock() if retained { t.Fatal("rollback panic retained attachment record") @@ -661,7 +663,8 @@ func TestAttachmentCompletionRetiresRecordAcrossRollbackClosePanic(t *testing.T) } cleanupOnlyInstance := new(wago.Instance) - cleanupOnlyAttempt, err := manager.beginAttachment(cleanupOnlyInstance) + cleanupOnlyKey := instanceKey{direct: cleanupOnlyInstance} + cleanupOnlyAttempt, err := manager.beginAttachment(cleanupOnlyKey) if err != nil { t.Fatal(err) } @@ -670,7 +673,7 @@ func TestAttachmentCompletionRetiresRecordAcrossRollbackClosePanic(t *testing.T) recovered = nil func() { defer func() { recovered = recover() }() - manager.completeAttachment(cleanupOnlyInstance, cleanupOnlyAttempt, cleanupOnlyState, false, nil, false) + manager.completeAttachment(cleanupOnlyKey, cleanupOnlyAttempt, cleanupOnlyState, false, nil, false) }() if recovered != cleanupPanic { t.Fatalf("cleanup-only panic = %v, want %v", recovered, cleanupPanic) @@ -679,7 +682,7 @@ func TestAttachmentCompletionRetiresRecordAcrossRollbackClosePanic(t *testing.T) t.Fatalf("cleanup-only quota usage=%+v closed=%v", usage, closed) } manager.mu.RLock() - _, retained = manager.attaching[cleanupOnlyInstance] + _, retained = manager.attaching[cleanupOnlyKey] manager.mu.RUnlock() if retained { t.Fatal("cleanup-only panic retained attachment record") @@ -841,8 +844,8 @@ func TestDetachCoordinatesWaitersReattachmentAndUnrelatedInstances(t *testing.T) t.Fatalf("first backend close count = %d, want 1", firstBackend.closed.Load()) } manager.mu.RLock() - _, attaching := manager.attaching[instance] - _, detaching := manager.detaching[instance] + _, attaching := manager.attaching[instanceKey{direct: instance}] + _, detaching := manager.detaching[instanceKey{direct: instance}] manager.mu.RUnlock() if attaching || detaching { t.Fatalf("completed lifecycle retained records attaching=%v detaching=%v", attaching, detaching) @@ -966,7 +969,7 @@ func TestDetachPanicRestoresInvariantsAndCoordinatesWaiters(t *testing.T) { t.Fatalf("old state retained teardown scratch namespace=%v poll=%v output=%v", oldState.NamespaceHandle(), oldState.pollEvents, oldState.outputScratch) } manager.mu.RLock() - _, retained := manager.detaching[instance] + _, retained := manager.detaching[instanceKey{direct: instance}] manager.mu.RUnlock() if retained { t.Fatal("panicking teardown retained detachment record") diff --git a/internal/plugin/binding_test.go b/internal/plugin/binding_test.go index f589357..41ca14c 100644 --- a/internal/plugin/binding_test.go +++ b/internal/plugin/binding_test.go @@ -13,7 +13,7 @@ func TestRegisterBindingsAcceptsCompleteBackendNeutralTable(t *testing.T) { Params: []wago.ValType{wago.ValI32}, Results: []wago.ValType{wago.ValI32}, Capability: "net.test", Docs: "bounded test binding", }} - RegisterBindings(new(wago.Registry).ImportModule("wago_net_test"), bindings) + RegisterBindings(new(wago.ImportModuleBuilder), bindings) if called { t.Fatal("registration invoked host function") } diff --git a/internal/plugin/host.go b/internal/plugin/host.go index c89bc09..380cdca 100644 --- a/internal/plugin/host.go +++ b/internal/plugin/host.go @@ -10,18 +10,32 @@ import ( // remain private so protocol modules cannot replace lifecycle ownership. type Host struct { instances *instance.Manager + callers *wago.CallerResolver } -// NewHost binds protocol modules to one extension-local instance manager. +// NewHost binds protocol modules to one fixture-local instance manager. func NewHost(instances *instance.Manager) Host { return Host{instances: instances} } +// NewRuntimeHost binds protocol modules to the opaque caller identity resolver +// granted to the owning Wago plugin. +func NewRuntimeHost(instances *instance.Manager, callers *wago.CallerResolver) Host { + return Host{instances: instances, callers: callers} +} + // State resolves networking state only for the exact calling Runtime instance. // HostModule-only mocks and detached instances fail closed. func (h Host) State(module wago.HostModule) (*instance.State, bool) { if h.instances == nil || module == nil { return nil, false } + if h.callers != nil { + identity, err := h.callers.Resolve(module) + if err != nil { + return nil, false + } + return h.instances.ForIdentity(identity) + } return h.instances.FromHost(module) } diff --git a/internal/plugin/registry.go b/internal/plugin/registry.go index f3b65b4..579de00 100644 --- a/internal/plugin/registry.go +++ b/internal/plugin/registry.go @@ -1,5 +1,5 @@ // Package plugin provides the implementation-neutral protocol composition -// contract used by the root networking extension and its protocol subpackages. +// contract used by the root networking provider and its protocol subpackages. package plugin import ( @@ -94,14 +94,14 @@ func NewAuthority(config policy.Config) Authority { // select them through tcp.Register, udp.Register, or dns.Register. type Module struct { key ModuleKey - install func(*wago.Registry, Host) + install func(*Registrar, Host) backend Backend authority Authority } // NewModule constructs one protocol descriptor. It is intentionally available // only below github.com/wago-org/net's internal-package boundary. -func NewModule(key ModuleKey, install func(*wago.Registry, Host), backend ...Backend) Module { +func NewModule(key ModuleKey, install func(*Registrar, Host), backend ...Backend) Module { module := Module{key: key, install: install} if len(backend) == 1 { module.backend = backend[0] @@ -131,10 +131,39 @@ func (m Module) ConfigureAuthority(target *policy.Config) error { return nil } -// Install contributes this module's capability and imports to a Wago registry -// using the exact shared instance host owned by the root network. -func (m Module) Install(registry *wago.Registry, host Host) { +// Registrar keeps protocol descriptors independent of Wago's public registrar +// shape while preserving exact authority failures for the aggregate plugin. +type Registrar struct { + reg *wago.Registrar + imports *wago.HostImportRegistrar + err error +} + +func (r *Registrar) Capability(cap wago.Capability, options ...wago.CapabilityOption) { + if r == nil || r.err != nil { + return + } + r.err = r.reg.GuestCapability(cap, options...) +} + +func (r *Registrar) ImportModule(name string) *wago.ImportModuleBuilder { + if r == nil || r.err != nil { + return new(wago.ImportModuleBuilder) + } + module, err := r.imports.Module(name) + if err != nil { + r.err = err + return new(wago.ImportModuleBuilder) + } + return module +} + +// Install contributes this module's capability and imports through the exact +// reviewed host-import authority scope. +func (m Module) Install(reg *wago.Registrar, imports *wago.HostImportRegistrar, host Host) error { + registry := &Registrar{reg: reg, imports: imports} m.install(registry, host) + return registry.err } // ConfigureBackend contributes protocol-local requirements before the shared diff --git a/internal/plugin/registry_test.go b/internal/plugin/registry_test.go index d30eac3..beff1c8 100644 --- a/internal/plugin/registry_test.go +++ b/internal/plugin/registry_test.go @@ -12,7 +12,7 @@ import ( ) func TestSetRejectsInvalidDuplicateAndFrozenModules(t *testing.T) { - install := func(*wago.Registry, Host) {} + install := func(*Registrar, Host) {} var set Set if err := set.Add(NewModule("", install)); !errors.Is(err, ErrInvalidModule) { t.Fatalf("empty key = %v", err) @@ -52,7 +52,7 @@ func TestBackendContributionConfigurationAndInstallation(t *testing.T) { return nscore.Service{Key: "tcp", Value: serviceValue}, nil }, ) - module := NewModule(ModuleTCP, func(*wago.Registry, Host) {}, backend) + module := NewModule(ModuleTCP, func(*Registrar, Host) {}, backend) var config backendConfig if err := module.ConfigureBackend(BackendLnetoV1, &config); err != nil || config.count != 1 { t.Fatalf("ConfigureBackend = %v, config=%+v", err, config) @@ -68,7 +68,7 @@ func TestBackendContributionConfigurationAndInstallation(t *testing.T) { t.Fatalf("incompatible install = %v", err) } - stateless := NewModule(ModuleDNS, func(*wago.Registry, Host) {}) + stateless := NewModule(ModuleDNS, func(*Registrar, Host) {}) if err := stateless.ConfigureBackend(BackendLnetoV1, &config); err != nil { t.Fatalf("stateless configure = %v", err) } @@ -82,7 +82,7 @@ func TestAuthorityContributionCopiesAndComposes(t *testing.T) { Action: policy.ActionAllow, Transports: []policy.Transport{policy.TransportTCP}, Directions: []policy.Direction{policy.DirectionOutbound}, }}, AllowLoopback: true} - module := NewModule(ModuleTCP, func(*wago.Registry, Host) {}).WithAuthority(NewAuthority(input)) + module := NewModule(ModuleTCP, func(*Registrar, Host) {}).WithAuthority(NewAuthority(input)) input.Rules[0].Transports[0] = policy.TransportUDP input.AllowLoopback = false @@ -99,7 +99,7 @@ func TestAuthorityContributionCopiesAndComposes(t *testing.T) { } func TestSetRejectsInvalidBackendContributions(t *testing.T) { - install := func(*wago.Registry, Host) {} + install := func(*Registrar, Host) {} for _, module := range []Module{ NewModule(ModuleTCP, install, NewBackend("", nil, func(any) (nscore.Service, error) { return nscore.Service{}, nil })), NewModule(ModuleTCP, install, NewBackend(BackendLnetoV1, nil, nil)), @@ -188,13 +188,13 @@ func TestModuleInstallForwardsExactHostFacade(t *testing.T) { manager := instancecore.NewManager() host := NewHost(manager) called := false - module := NewModule(ModuleICMPv4, func(registry *wago.Registry, got Host) { + module := NewModule(ModuleICMPv4, func(registry *Registrar, got Host) { called = true if registry == nil || got.instances != manager { t.Fatalf("Install host = %+v registry=%p", got, registry) } }) - module.Install(new(wago.Registry), host) + module.install(&Registrar{}, host) if !called { t.Fatal("module installer was not called") } @@ -235,7 +235,7 @@ func BenchmarkHostStateUnattached(b *testing.B) { } func TestFreezeReturnsIndependentStableSnapshots(t *testing.T) { - install := func(*wago.Registry, Host) {} + install := func(*Registrar, Host) {} var set Set if err := set.Add(NewModule(ModuleTCP, install)); err != nil { t.Fatalf("Add TCP: %v", err) diff --git a/internal/plugintest/plugintest.go b/internal/plugintest/plugintest.go new file mode 100644 index 0000000..3257797 --- /dev/null +++ b/internal/plugintest/plugintest.go @@ -0,0 +1,126 @@ +// Package plugintest contains repository-internal vNext plugin test helpers. +package plugintest + +import ( + "context" + "fmt" + + wagonet "github.com/wago-org/net" + wago "github.com/wago-org/wago" + "github.com/wago-org/wago/src/core/compiler/wasm" + "github.com/wago-org/wago/tests/wasmtest" +) + +var allModules = []string{ + wagonet.Module, wagonet.UDPModule, wagonet.TCPModule, wagonet.DNSModule, + wagonet.ICMPv4Module, wagonet.NTPModule, wagonet.MDNSModule, + wagonet.DHCPv4Module, wagonet.LinkLocal4Module, wagonet.IPv6Module, + wagonet.ICMPv6Module, wagonet.DHCPv6Module, wagonet.TLSModule, +} + +// LoadNetwork loads one already-composed network through the same reviewed +// authority and explicit-provider path used in production. +func LoadNetwork(runtime *wago.Runtime, network *wagonet.Network) error { + provider := wagonet.Provider(wagonet.ProviderSpec{ + ID: "example.com/wagonet/test", Name: "Networking test composition", + Description: "Repository-local exact networking composition", + Modules: allModules, + Factory: func() (*wagonet.Network, error) { return network, nil }, + }) + return runtime.LoadPlugins(context.Background(), Set(provider)) +} + +// Set selects providers with their exact definition digests and requested grants. +func Set(providers ...wago.PluginProvider) wago.PluginSet { + set := wago.PluginSet{Providers: providers} + for _, provider := range providers { + digest, err := wago.DefinitionDigest(provider.Definition) + if err != nil { + panic(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 + } + for _, authority := range provider.Definition.Authorities { + selection.Grants = append(selection.Grants, wago.AuthorityGrant{Name: authority.Name, Scope: authority.Scope}) + } + set.Selections = append(set.Selections, selection) + } + return set +} + +func HasImport(runtime *wago.Runtime, module, name string) bool { + for _, spec := range runtime.ProvidedImports() { + if spec.Module == module && spec.Name == name { + return true + } + } + return false +} + +// CompileImportHarness compiles one-memory guest that re-exports every host +// function provided in module. Tests exercise the real guest-call path instead +// of reaching into Runtime for callable imports. +func CompileImportHarness(runtime *wago.Runtime, module string) (*wago.Module, error) { + var specs []wago.ImportSpec + for _, spec := range runtime.ProvidedImports() { + if spec.Module == module { + specs = append(specs, spec) + } + } + if len(specs) == 0 { + return nil, fmt.Errorf("plugintest: no imports for module %q", module) + } + types := make([][]byte, 0, len(specs)) + imports := make([][]byte, 0, len(specs)) + exports := make([][]byte, 0, len(specs)+1) + for i, spec := range specs { + params, err := wasmTypes(spec.Params) + if err != nil { + return nil, err + } + results, err := wasmTypes(spec.Results) + if err != nil { + return nil, err + } + types = append(types, wasmtest.FuncType(params, results)) + entry := append(append(wasmtest.Name(spec.Module), wasmtest.Name(spec.Name)...), 0x00) + entry = append(entry, wasmtest.ULEB(uint32(i))...) + imports = append(imports, entry) + export := append(wasmtest.Name(spec.Name), 0x00) + export = append(export, wasmtest.ULEB(uint32(i))...) + exports = append(exports, export) + } + memoryExport := append(wasmtest.Name("memory"), 0x02, 0x00) + exports = append(exports, memoryExport) + source := wasmtest.Module( + wasmtest.Section(1, wasmtest.Vec(types...)), + wasmtest.Section(2, wasmtest.Vec(imports...)), + wasmtest.Section(5, wasmtest.Vec([]byte{0x00, 0x01})), + wasmtest.Section(7, wasmtest.Vec(exports...)), + ) + return runtime.Compile(source) +} + +func wasmTypes(types []wago.ValType) ([]wasm.ValType, error) { + out := make([]wasm.ValType, len(types)) + for i, value := range types { + switch value { + case wago.ValI32: + out[i] = wasm.I32 + case wago.ValI64: + out[i] = wasm.I64 + case wago.ValF32: + out[i] = wasm.F32 + case wago.ValF64: + out[i] = wasm.F64 + default: + return nil, fmt.Errorf("plugintest: unsupported harness value type %v", value) + } + } + return out, nil +} diff --git a/ipv6/ipv6_test.go b/ipv6/ipv6_test.go index d2589a1..33a7da2 100644 --- a/ipv6/ipv6_test.go +++ b/ipv6/ipv6_test.go @@ -10,7 +10,6 @@ import ( wagonet "github.com/wago-org/net" ipv6abi "github.com/wago-org/net/internal/abi/ipv6" - ipv6binding "github.com/wago-org/net/internal/binding/ipv6" "github.com/wago-org/net/internal/guest" ipv6ns "github.com/wago-org/net/internal/namespace/ipv6" "github.com/wago-org/net/internal/resource" @@ -23,7 +22,7 @@ func TestSelectiveRegistrationAndFiniteConfiguration(t *testing.T) { t.Fatal(err) } runtime := wago.NewRuntime() - if err := runtime.Use(network); err != nil { + if err := loadNetwork(runtime, network); err != nil { t.Fatal(err) } if got := runtime.Capabilities(); !reflect.DeepEqual(got, []wago.Capability{wagonet.CapInfo, wagonet.CapIPv6}) { @@ -90,10 +89,10 @@ func TestConfiguredIPv6FailsClosedBeforePublication(t *testing.T) { t.Fatal(err) } runtime := wago.NewRuntime() - if err := runtime.Use(network); err != nil { + if err := loadNetwork(runtime, network); err != nil { t.Fatal(err) } - module, err := runtime.Compile([]byte{0x00, 0x61, 0x73, 0x6d, 0x01, 0, 0, 0}) + module, err := compileImportHarness(runtime) if err != nil { t.Fatal(err) } @@ -117,7 +116,7 @@ func TestConfiguredIPv6RejectsTerminalCoreConfiguration(t *testing.T) { t.Fatal(err) } runtime := wago.NewRuntime() - if err := runtime.Use(network); err == nil { + if err := loadNetwork(runtime, network); err == nil { t.Fatal("IPv6 contribution accepted an MTU below 1280") } } @@ -142,7 +141,7 @@ func TestActualLinkLocalConfigurationPreservesNumericScope(t *testing.T) { t.Fatal(err) } runtime, host := instantiate(t, network) - if _, ok := runtime.HostImports()[wagonet.TCPModule+".namespace_default"]; ok { + if hasImport(runtime, wagonet.TCPModule, "namespace_default") { t.Fatal("IPv6 configuration exposed an unselected TCP import") } if got := callImport(t, runtime, host, "namespace_default", 200); got != guest.StatusOK { @@ -215,10 +214,10 @@ func TestActualCheckedConfigurationAndDisabledTruth(t *testing.T) { func instantiate(t testing.TB, network *wagonet.Network) (*wago.Runtime, hostModule) { t.Helper() runtime := wago.NewRuntime() - if err := runtime.Use(network); err != nil { + if err := loadNetwork(runtime, network); err != nil { t.Fatal(err) } - module, err := runtime.Compile([]byte{0x00, 0x61, 0x73, 0x6d, 0x01, 0, 0, 0}) + module, err := compileImportHarness(runtime) if err != nil { t.Fatal(err) } @@ -227,16 +226,14 @@ func instantiate(t testing.TB, network *wagonet.Network) (*wago.Runtime, hostMod t.Fatal(err) } t.Cleanup(func() { _ = instance.Close() }) - return runtime, hostModule{instance: instance, memory: make([]byte, 256)} + return runtime, hostModule{instance: instance, memory: instance.Memory().Bytes()} } -func callImport(t testing.TB, runtime *wago.Runtime, host hostModule, name string, params ...uint64) guest.Status { +func callImport(t testing.TB, _ *wago.Runtime, host hostModule, name string, params ...uint64) guest.Status { t.Helper() - function, ok := runtime.HostImports()[ipv6binding.Module+"."+name].(wago.HostFunc) - if !ok { - t.Fatalf("IPv6 import %q missing", name) + results, err := host.instance.Invoke(name, params...) + if err != nil || len(results) != 1 { + t.Fatalf("IPv6 import %q = %v, %v", name, results, err) } - var results [1]uint64 - function(host, params, results[:]) return guest.Status(int32(results[0])) } diff --git a/ipv6/plugin_test.go b/ipv6/plugin_test.go new file mode 100644 index 0000000..b2ef0e2 --- /dev/null +++ b/ipv6/plugin_test.go @@ -0,0 +1,19 @@ +package ipv6 + +import ( + wagonet "github.com/wago-org/net" + "github.com/wago-org/net/internal/plugintest" + wago "github.com/wago-org/wago" +) + +func loadNetwork(runtime *wago.Runtime, network *wagonet.Network) error { + return plugintest.LoadNetwork(runtime, network) +} + +func hasImport(runtime *wago.Runtime, module, name string) bool { + return plugintest.HasImport(runtime, module, name) +} + +func compileImportHarness(runtime *wago.Runtime) (*wago.Module, error) { + return plugintest.CompileImportHarness(runtime, wagonet.IPv6Module) +} diff --git a/ipv6/register/register.go b/ipv6/register/register.go index 8082066..8825ae0 100644 --- a/ipv6/register/register.go +++ b/ipv6/register/register.go @@ -1,5 +1,4 @@ -// Package register self-registers the selective IPv6 namespace extension. -// Deployment identity remains disabled until explicit composition supplies it. +// Package register exposes the explicit IPv6 networking catalog entry. package register import ( @@ -8,12 +7,15 @@ import ( wago "github.com/wago-org/wago" ) -func init() { - wago.RegisterExtension("net-ipv6", func() wago.Extension { - network := wagonet.New() - if err := ipv6.Register(network); err != nil { - panic("wagonet/ipv6/register: " + err.Error()) - } - return network +func Provider() wago.PluginProvider { + return wagonet.Provider(wagonet.ProviderSpec{ + ID: "github.com/wago-org/net/ipv6", Name: "Wago IPv6", Description: "Bounded checked IPv6 networking.", + Modules: []string{wagonet.Module, wagonet.IPv6Module}, + Factory: func() (*wagonet.Network, error) { + network := wagonet.New() + return network, ipv6.Register(network) + }, }) } + +func Providers() []wago.PluginProvider { return []wago.PluginProvider{Provider()} } diff --git a/ipv6/register/register_test.go b/ipv6/register/register_test.go index 8114fd2..86bdb06 100644 --- a/ipv6/register/register_test.go +++ b/ipv6/register/register_test.go @@ -6,20 +6,23 @@ import ( "testing" wagonet "github.com/wago-org/net" - _ "github.com/wago-org/net/ipv6/register" + "github.com/wago-org/net/internal/plugintest" + netregister "github.com/wago-org/net/ipv6/register" wago "github.com/wago-org/wago" "github.com/wago-org/wago/src/core/compiler/wasm" - "github.com/wago-org/wago/testutil/wasmtest" + "github.com/wago-org/wago/tests/wasmtest" ) func TestIPv6FactoryHasExactRuntimeSurface(t *testing.T) { - extension, ok := wago.NewExtension("net-ipv6") - if !ok { - t.Fatal("IPv6-only extension was not registered") + providers := netregister.Providers() + if len(providers) != 1 { + t.Fatalf("Providers = %d, want 1", len(providers)) } + provider := providers[0] runtime := wago.NewRuntime() - if err := runtime.Use(extension); err != nil { - t.Fatalf("Use: %v", err) + defer runtime.Close() + if err := runtime.LoadPlugins(context.Background(), plugintest.Set(provider)); err != nil { + t.Fatalf("LoadPlugins: %v", err) } if got, want := runtime.Capabilities(), []wago.Capability{wagonet.CapInfo, wagonet.CapIPv6}; !reflect.DeepEqual(got, want) { t.Fatalf("capabilities = %v, want %v", got, want) diff --git a/lifecycle_test.go b/lifecycle_test.go deleted file mode 100644 index 5c5b506..0000000 --- a/lifecycle_test.go +++ /dev/null @@ -1,493 +0,0 @@ -package net - -import ( - "context" - "errors" - "net/netip" - "sync" - "sync/atomic" - "testing" - - tcpinstance "github.com/wago-org/net/internal/instance/tcp" - udpinstance "github.com/wago-org/net/internal/instance/udp" - "github.com/wago-org/net/internal/namespace" - "github.com/wago-org/net/internal/quota" - "github.com/wago-org/net/internal/readiness" - "github.com/wago-org/net/internal/resource" - wago "github.com/wago-org/wago" - "github.com/wago-org/wago/testutil/wasmtest" -) - -type lifecycleResource struct { - closed atomic.Int32 -} - -func (r *lifecycleResource) Close() error { - if r.closed.Add(1) != 1 { - panic("lifecycle resource closed more than once") - } - return nil -} - -type observerExtension struct { - fn wago.HostFunc -} - -func (observerExtension) Info() wago.ExtensionInfo { - return wago.ExtensionInfo{ID: "test.net-observer", Version: "1.0.0", Stability: wago.Stable} -} - -func (e observerExtension) Register(reg *wago.Registry) error { - reg.ImportModule("env").Func("observe", e.fn) - return nil -} - -type failingSetupExtension struct { - netup *Extension - resource *lifecycleResource - err error -} - -func (failingSetupExtension) Info() wago.ExtensionInfo { - return wago.ExtensionInfo{ID: "test.net-failing-setup", Version: "1.0.0", Stability: wago.Stable} -} - -func (e failingSetupExtension) Register(reg *wago.Registry) error { - reg.Hooks().AfterInstantiate(func(_ *wago.InstantiateContext, instance *wago.Instance) error { - state, ok := e.netup.instanceManager().ForInstance(instance) - if !ok { - return errors.New("networking state was not attached before later setup") - } - if _, err := state.Resources().Add(resource.KindNamespace, e.resource); err != nil { - return err - } - return e.err - }) - return nil -} - -func emptyModule(t *testing.T, runtime *wago.Runtime) *wago.Module { - t.Helper() - module, err := runtime.Compile([]byte{0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00}) - if err != nil { - t.Fatalf("Compile empty module: %v", err) - } - return module -} - -func observerModule(t *testing.T, runtime *wago.Runtime) *wago.Module { - t.Helper() - importEntry := append(append(wasmtest.Name("env"), wasmtest.Name("observe")...), 0x00, 0x00) - wasm := wasmtest.Module( - wasmtest.Section(1, wasmtest.Vec(wasmtest.FuncType(nil, nil))), - wasmtest.Section(2, wasmtest.Vec(importEntry)), - wasmtest.Section(3, wasmtest.Vec(wasmtest.ULEB(0))), - wasmtest.Section(7, wasmtest.Vec(wasmtest.ExportEntry("run", 0, 1))), - wasmtest.Section(10, wasmtest.Vec(wasmtest.Code([]byte{0x10, 0x00, 0x0b}))), - ) - module, err := runtime.Compile(wasm) - if err != nil { - t.Fatalf("Compile observer module: %v", err) - } - return module -} - -func TestInstanceStateIsolationHostIdentityAndCrossTableHandles(t *testing.T) { - extension := Init(Config{}) - manager := extension.instanceManager() - var observed []*wago.Instance - var observedState any - runtime := wago.NewRuntime() - if err := runtime.Use(extension); err != nil { - t.Fatalf("Use net extension: %v", err) - } - if err := runtime.Use(observerExtension{fn: func(module wago.HostModule, _, _ []uint64) { - identity, ok := module.(wago.InstanceHostModule) - if !ok { - t.Fatal("host module has no instance identity") - } - observed = append(observed, identity.Instance()) - state, ok := manager.FromHost(module) - if !ok { - t.Fatal("networking state missing for host caller") - } - observedState = state - }}); err != nil { - t.Fatalf("Use observer extension: %v", err) - } - module := observerModule(t, runtime) - first, err := runtime.Instantiate(context.Background(), module) - if err != nil { - t.Fatalf("Instantiate first: %v", err) - } - defer first.Close() - second, err := runtime.Instantiate(context.Background(), module) - if err != nil { - t.Fatalf("Instantiate second: %v", err) - } - defer second.Close() - - firstState, firstOK := manager.ForInstance(first) - secondState, secondOK := manager.ForInstance(second) - if !firstOK || !secondOK || firstState == secondState || firstState.Resources() == secondState.Resources() || firstState.Readiness() == secondState.Readiness() { - t.Fatalf("isolated states = (%p,%v) (%p,%v)", firstState, firstOK, secondState, secondOK) - } - if got := manager.Len(); got != 2 { - t.Fatalf("attached state count = %d, want 2", got) - } - - handle, err := firstState.Resources().Add(resource.KindUDPSocket, &lifecycleResource{}) - if err != nil { - t.Fatalf("Add first resource: %v", err) - } - if _, err := secondState.Resources().Lookup(handle, resource.KindUDPSocket); !errors.Is(err, resource.ErrBadHandle) { - t.Fatalf("cross-instance Lookup error = %v, want ErrBadHandle", err) - } - - if _, err := first.Call(context.Background(), "run"); err != nil { - t.Fatalf("Call first: %v", err) - } - if len(observed) != 1 || observed[0] != first || observedState != firstState { - t.Fatalf("first host identity/state = %v/%p, want %p/%p", observed, observedState, first, firstState) - } - if _, err := second.Call(context.Background(), "run"); err != nil { - t.Fatalf("Call second: %v", err) - } - if len(observed) != 2 || observed[1] != second || observedState != secondState { - t.Fatalf("second host identity/state = %v/%p, want %p/%p", observed, observedState, second, secondState) - } -} - -func TestConfiguredNamespaceUDPHandlesReadinessAndCleanup(t *testing.T) { - prefix := netip.MustParsePrefix("192.0.2.0/24") - limits := QuotaLimits{Resources: 2, UDPResources: 1, QueuedBytes: 64} - readinessConfig := ReadinessConfig{MaxRegistrations: 2} - extension := Init(Config{ - Policy: PolicyConfig{Rules: []PolicyRule{{ - Action: PolicyAllow, - Transports: []PolicyTransport{PolicyTransportUDP}, - Directions: []PolicyDirection{PolicyInbound, PolicyOutbound}, - Prefixes: []netip.Prefix{prefix}, - }}}, - Limits: &limits, - Readiness: &readinessConfig, - StaticIPv4: &StaticIPv4Config{ - Hostname: "instance1", - RandSeed: 1, - HardwareAddress: [6]byte{2, 0, 0, 0, 0, 1}, - GatewayHardwareAddress: [6]byte{2, 0, 0, 0, 0, 2}, - IPv4Address: netip.MustParseAddr("192.0.2.1"), - MTU: 1500, - Link: PacketLinkConfig{MaxFrameBytes: 1514, IngressFrames: 2, EgressFrames: 2}, - UDP: UDPConfig{ - MaxSockets: 1, - ReceiveBytes: 32, - TransmitBytes: 32, - ReceiveDatagrams: 1, - TransmitDatagrams: 1, - MaxPayloadBytes: 32, - }, - }, - }) - runtime := wago.NewRuntime() - if err := runtime.Use(extension); err != nil { - t.Fatalf("Use: %v", err) - } - firstInstance, err := runtime.Instantiate(context.Background(), emptyModule(t, runtime)) - if err != nil { - t.Fatalf("Instantiate first: %v", err) - } - secondInstance, err := runtime.Instantiate(context.Background(), emptyModule(t, runtime)) - if err != nil { - t.Fatalf("Instantiate second: %v", err) - } - defer secondInstance.Close() - first, _ := extension.instanceManager().ForInstance(firstInstance) - second, _ := extension.instanceManager().ForInstance(secondInstance) - local := namespace.Endpoint{Address: netip.MustParseAddr("192.0.2.1"), Port: 4100} - firstUDP, progress, err := udpinstance.Bind(first, first.NamespaceHandle(), local) - if err != nil || progress != namespace.ProgressDone || firstUDP == 0 { - t.Fatalf("first BindUDP = %v, %v, %v", firstUDP, progress, err) - } - secondUDP, progress, err := udpinstance.Bind(second, second.NamespaceHandle(), local) - if err != nil || progress != namespace.ProgressDone || secondUDP == 0 { - t.Fatalf("second BindUDP = %v, %v, %v", secondUDP, progress, err) - } - if _, err := second.Resources().Lookup(firstUDP, resource.KindUDPSocket); !errors.Is(err, resource.ErrBadHandle) { - t.Fatalf("cross-instance UDP lookup = %v", err) - } - if snapshot := first.Readiness().Snapshot(); snapshot.Registrations != 2 || snapshot.Capacity != 2 { - t.Fatalf("UDP readiness registration = %+v", snapshot) - } - events := make([]readiness.Event, 2) - budget := readiness.Budget{Scans: 2, Events: 2} - report, pollProgress, err := first.Readiness().TryPoll(events, budget) - if err != nil || pollProgress != namespace.ProgressDone || report.Events != 2 { - t.Fatalf("UDP poll = %+v, %v, %v, events=%+v", report, pollProgress, err, events) - } - seenUDP := false - for _, event := range events { - if event.Handle == firstUDP && event.Readiness == namespace.ReadyWritable { - seenUDP = true - } - } - if !seenUDP { - t.Fatalf("UDP writable event missing: %+v", events) - } - if err := first.CloseHandle(firstUDP, resource.KindTCPStream); !errors.Is(err, resource.ErrBadHandle) { - t.Fatalf("wrong-kind close = %v", err) - } - if snapshot := first.Readiness().Snapshot(); snapshot.Registrations != 2 { - t.Fatalf("wrong-kind close changed readiness = %+v", snapshot) - } - if err := first.CloseHandle(firstUDP, resource.KindUDPSocket); err != nil { - t.Fatalf("CloseHandle UDP: %v", err) - } - if _, err := first.Resources().Lookup(firstUDP, resource.KindUDPSocket); !errors.Is(err, resource.ErrBadHandle) { - t.Fatalf("stale UDP handle lookup = %v", err) - } - if usage, closed := first.Quotas().Snapshot(); closed || usage.Resources != 1 || usage.UDPResources != 0 || usage.QueuedBytes != 0 { - t.Fatalf("UDP cleanup quota = %+v, closed=%v", usage, closed) - } - rebound, progress, err := udpinstance.Bind(first, first.NamespaceHandle(), local) - if err != nil || progress != namespace.ProgressDone || rebound == 0 || rebound == firstUDP { - t.Fatalf("UDP rebound = %v, %v, %v", rebound, progress, err) - } - if err := firstInstance.Close(); err != nil { - t.Fatalf("Close first: %v", err) - } - if usage, closed := first.Quotas().Snapshot(); !closed || usage != (quota.Usage{}) { - t.Fatalf("first teardown quota = %+v, closed=%v", usage, closed) - } - if snapshot := first.Readiness().Snapshot(); !snapshot.Closed || snapshot.Registrations != 0 { - t.Fatalf("first teardown readiness = %+v", snapshot) - } - if _, err := first.Resources().Lookup(rebound, resource.KindUDPSocket); !errors.Is(err, resource.ErrClosed) { - t.Fatalf("teardown stale UDP lookup = %v", err) - } -} - -func TestInstanceStateCleanupOnRepeatedAndConcurrentClose(t *testing.T) { - for _, test := range []struct { - name string - close func(*wago.Instance) - }{ - {"repeated", func(instance *wago.Instance) { _ = instance.Close(); _ = instance.Close() }}, - {"concurrent", func(instance *wago.Instance) { - var wait sync.WaitGroup - for range 16 { - wait.Add(1) - go func() { - defer wait.Done() - _ = instance.Close() - }() - } - wait.Wait() - }}, - } { - t.Run(test.name, func(t *testing.T) { - extension := Init(Config{}) - manager := extension.instanceManager() - runtime := wago.NewRuntime() - if err := runtime.Use(extension); err != nil { - t.Fatalf("Use: %v", err) - } - instance, err := runtime.Instantiate(context.Background(), emptyModule(t, runtime)) - if err != nil { - t.Fatalf("Instantiate: %v", err) - } - state, ok := manager.ForInstance(instance) - if !ok { - t.Fatal("state not attached") - } - owned := &lifecycleResource{} - if _, err := state.Resources().Add(resource.KindTCPStream, owned); err != nil { - t.Fatalf("Add resource: %v", err) - } - test.close(instance) - if owned.closed.Load() != 1 { - t.Fatalf("resource close count = %d, want 1", owned.closed.Load()) - } - if manager.Len() != 0 { - t.Fatalf("attached states after close = %d, want 0", manager.Len()) - } - if _, ok := manager.ForInstance(instance); ok { - t.Fatal("closed instance retained state") - } - }) - } -} - -func TestInstanceStateCleanupClosesQuotaLedger(t *testing.T) { - extension := Init(Config{}) - manager := extension.instanceManager() - runtime := wago.NewRuntime() - if err := runtime.Use(extension); err != nil { - t.Fatalf("Use: %v", err) - } - instance, err := runtime.Instantiate(context.Background(), emptyModule(t, runtime)) - if err != nil { - t.Fatalf("Instantiate: %v", err) - } - state, ok := manager.ForInstance(instance) - if !ok { - t.Fatal("state not attached") - } - poller := state.Readiness() - if poller == nil { - t.Fatal("readiness coordinator not attached") - } - pending, err := state.Quotas().ReserveQueuedBytes(4096) - if err != nil { - t.Fatalf("ReserveQueuedBytes: %v", err) - } - reservation, err := state.Quotas().ReserveDNSWork(1) - if err != nil { - t.Fatalf("ReserveDNSWork: %v", err) - } - allocation, ok := reservation.Commit() - if !ok { - t.Fatal("Commit DNS work failed") - } - if err := instance.Close(); err != nil { - t.Fatalf("Close: %v", err) - } - if usage, closed := state.Quotas().Snapshot(); !closed || usage != (quota.Usage{}) { - t.Fatalf("quota after instance close = %+v, closed=%v", usage, closed) - } - if snapshot := poller.Snapshot(); !snapshot.Closed || snapshot.Registrations != 0 { - t.Fatalf("readiness after instance close = %+v", snapshot) - } - if _, err := state.Quotas().ReserveService(1); !errors.Is(err, quota.ErrClosed) { - t.Fatalf("reserve after instance close error = %v", err) - } - if !pending.Rollback() || !allocation.Release() { - t.Fatal("late token cleanup was not locally accepted") - } -} - -func TestInstanceStateCleanupAfterLaterSetupFailure(t *testing.T) { - setupErr := errors.New("later extension setup failed") - extension := Init(Config{}) - owned := &lifecycleResource{} - runtime := wago.NewRuntime() - if err := runtime.Use(extension); err != nil { - t.Fatalf("Use net extension: %v", err) - } - if err := runtime.Use(failingSetupExtension{netup: extension, resource: owned, err: setupErr}); err != nil { - t.Fatalf("Use failing extension: %v", err) - } - if _, err := runtime.Instantiate(context.Background(), emptyModule(t, runtime)); !errors.Is(err, setupErr) { - t.Fatalf("Instantiate error = %v, want %v", err, setupErr) - } - if owned.closed.Load() != 1 { - t.Fatalf("failed-setup resource close count = %d, want 1", owned.closed.Load()) - } - if got := extension.instanceManager().Len(); got != 0 { - t.Fatalf("attached states after failed setup = %d, want 0", got) - } -} - -func TestNetworkingRequirementReinstantiatesSnapshotClass(t *testing.T) { - limits := QuotaLimits{Resources: 4, UDPResources: 1, TCPResources: 2, QueuedBytes: 2048, ServiceUnits: 64} - ready := ReadinessConfig{MaxRegistrations: 4} - prefix := netip.MustParsePrefix("192.0.2.0/24") - extension := Init(Config{ - Policy: PolicyConfig{Rules: []PolicyRule{{ - Action: PolicyAllow, Transports: []PolicyTransport{PolicyTransportUDP, PolicyTransportTCP}, - Directions: []PolicyDirection{PolicyInbound, PolicyOutbound}, Prefixes: []netip.Prefix{prefix}, - }}}, - Limits: &limits, Readiness: &ready, - StaticIPv4: &StaticIPv4Config{ - Hostname: "pooled-net", RandSeed: 71, - HardwareAddress: [6]byte{2, 0, 0, 0, 0, 71}, GatewayHardwareAddress: [6]byte{2, 0, 0, 0, 0, 72}, - IPv4Address: netip.MustParseAddr("192.0.2.71"), MTU: 1500, - Link: PacketLinkConfig{MaxFrameBytes: 1514, IngressFrames: 8, EgressFrames: 8}, - UDP: UDPConfig{MaxSockets: 1, ReceiveBytes: 64, TransmitBytes: 64, ReceiveDatagrams: 2, TransmitDatagrams: 2, MaxPayloadBytes: 32}, - TCP: TCPConfig{MaxListeners: 1, MaxOutboundStreams: 1, AcceptBacklog: 1, ReceiveBytes: 256, TransmitBytes: 256, TransmitPackets: 4}, - }, - }) - manager := extension.instanceManager() - runtime := wago.NewRuntime() - if err := runtime.Use(extension); err != nil { - t.Fatalf("Use: %v", err) - } - class, err := runtime.Class(emptyModule(t, runtime), wago.ClassOptions{ - Pool: wago.PoolOptions{MinInstances: 1, MaxInstances: 1, Reset: wago.ResetMemorySnapshot}, - }) - if err != nil { - t.Fatalf("Class: %v", err) - } - if got := class.ResetPolicy(); got != wago.ResetReinstantiate { - t.Fatalf("effective reset policy = %v, want reinstantiate", got) - } - - lease, err := class.Acquire(context.Background()) - if err != nil { - t.Fatalf("Acquire: %v", err) - } - oldInstance := lease.Instance() - oldState, ok := manager.ForInstance(oldInstance) - if !ok { - t.Fatal("old instance state not attached") - } - localUDP := namespace.Endpoint{Address: netip.MustParseAddr("192.0.2.71"), Port: 4100} - udpHandle, progress, err := udpinstance.Bind(oldState, oldState.NamespaceHandle(), localUDP) - if err != nil || progress != namespace.ProgressDone { - t.Fatalf("BindUDP = %v, %v, %v", udpHandle, progress, err) - } - localTCP := namespace.Endpoint{Address: netip.MustParseAddr("192.0.2.71"), Port: 4200} - tcpHandle, progress, err := tcpinstance.Listen(oldState, oldState.NamespaceHandle(), localTCP) - if err != nil || progress != namespace.ProgressDone { - t.Fatalf("ListenTCP = %v, %v, %v", tcpHandle, progress, err) - } - if err := lease.Release(); err != nil { - t.Fatalf("Release: %v", err) - } - if _, ok := manager.ForInstance(oldInstance); ok { - t.Fatal("snapshot-configured class retained old networking state") - } - if _, err := oldState.Resources().Lookup(udpHandle, resource.KindUDPSocket); !errors.Is(err, resource.ErrClosed) { - t.Fatalf("old UDP handle after release = %v, want ErrClosed", err) - } - if _, err := oldState.Resources().Lookup(tcpHandle, resource.KindTCPListener); !errors.Is(err, resource.ErrClosed) { - t.Fatalf("old TCP handle after release = %v, want ErrClosed", err) - } - if usage, closed := oldState.Quotas().Snapshot(); !closed || usage != (quota.Usage{}) { - t.Fatalf("old quota after release = %+v, closed=%v", usage, closed) - } - - freshLease, err := class.Acquire(context.Background()) - if err != nil { - t.Fatalf("Acquire fresh: %v", err) - } - freshInstance := freshLease.Instance() - freshState, ok := manager.ForInstance(freshInstance) - if !ok || freshInstance == oldInstance || freshState == oldState { - t.Fatalf("fresh instance/state = %p/%p, old = %p/%p, attached=%v", freshInstance, freshState, oldInstance, oldState, ok) - } - if _, err := freshState.Resources().Lookup(udpHandle, resource.KindUDPSocket); !errors.Is(err, resource.ErrBadHandle) { - t.Fatalf("old UDP handle in fresh lease = %v, want ErrBadHandle", err) - } - if _, err := freshState.Resources().Lookup(tcpHandle, resource.KindTCPListener); !errors.Is(err, resource.ErrBadHandle) { - t.Fatalf("old TCP handle in fresh lease = %v, want ErrBadHandle", err) - } - if usage, closed := freshState.Quotas().Snapshot(); closed || usage.Resources != 1 || usage.UDPResources != 0 || usage.TCPResources != 0 { - t.Fatalf("fresh quota before resources = %+v, closed=%v", usage, closed) - } - if rebound, progress, err := udpinstance.Bind(freshState, freshState.NamespaceHandle(), localUDP); err != nil || progress != namespace.ProgressDone || rebound == 0 { - t.Fatalf("fresh BindUDP = %v, %v, %v", rebound, progress, err) - } - if relistened, progress, err := tcpinstance.Listen(freshState, freshState.NamespaceHandle(), localTCP); err != nil || progress != namespace.ProgressDone || relistened == 0 { - t.Fatalf("fresh ListenTCP = %v, %v, %v", relistened, progress, err) - } - if err := freshLease.Release(); err != nil { - t.Fatalf("Release fresh: %v", err) - } - if err := class.Close(); err != nil { - t.Fatalf("Class.Close: %v", err) - } - if got := manager.Len(); got != 0 { - t.Fatalf("states after Class.Close = %d, want 0", got) - } -} diff --git a/linklocal4/linklocal4_test.go b/linklocal4/linklocal4_test.go index 847ec57..14429b5 100644 --- a/linklocal4/linklocal4_test.go +++ b/linklocal4/linklocal4_test.go @@ -11,7 +11,6 @@ import ( wagonet "github.com/wago-org/net" linklocalabi "github.com/wago-org/net/internal/abi/linklocal4" - linklocalbinding "github.com/wago-org/net/internal/binding/linklocal4" "github.com/wago-org/net/internal/guest" linklocalns "github.com/wago-org/net/internal/namespace/linklocal4" "github.com/wago-org/net/internal/resource" @@ -28,7 +27,7 @@ func TestSelectiveRegistrationAndExplicitFiniteConfiguration(t *testing.T) { t.Fatal(err) } runtime := wago.NewRuntime() - if err := runtime.Use(network); err != nil { + if err := loadNetwork(runtime, network); err != nil { t.Fatal(err) } if got := runtime.Capabilities(); !reflect.DeepEqual(got, []wago.Capability{wagonet.CapInfo, wagonet.CapLinkLocal4}) { @@ -122,10 +121,10 @@ func TestActualBackendExactLifecycleAndDenyWins(t *testing.T) { func instantiate(t testing.TB, network *wagonet.Network) (*wago.Runtime, hostModule) { t.Helper() runtime := wago.NewRuntime() - if err := runtime.Use(network); err != nil { + if err := loadNetwork(runtime, network); err != nil { t.Fatal(err) } - module, err := runtime.Compile([]byte{0x00, 0x61, 0x73, 0x6d, 0x01, 0, 0, 0}) + module, err := compileImportHarness(runtime) if err != nil { t.Fatal(err) } @@ -134,16 +133,14 @@ func instantiate(t testing.TB, network *wagonet.Network) (*wago.Runtime, hostMod t.Fatal(err) } t.Cleanup(func() { _ = instance.Close() }) - return runtime, hostModule{instance: instance, memory: make([]byte, 256)} + return runtime, hostModule{instance: instance, memory: instance.Memory().Bytes()} } -func callImport(t testing.TB, runtime *wago.Runtime, host hostModule, name string, params ...uint64) guest.Status { +func callImport(t testing.TB, _ *wago.Runtime, host hostModule, name string, params ...uint64) guest.Status { t.Helper() - function, ok := runtime.HostImports()[linklocalbinding.Module+"."+name].(wago.HostFunc) - if !ok { - t.Fatalf("link-local import %q missing", name) + results, err := host.instance.Invoke(name, params...) + if err != nil || len(results) != 1 { + t.Fatalf("link-local import %q = %v, %v", name, results, err) } - var results [1]uint64 - function(host, params, results[:]) return guest.Status(int32(results[0])) } diff --git a/linklocal4/plugin_test.go b/linklocal4/plugin_test.go new file mode 100644 index 0000000..a2d9bde --- /dev/null +++ b/linklocal4/plugin_test.go @@ -0,0 +1,15 @@ +package linklocal4 + +import ( + wagonet "github.com/wago-org/net" + "github.com/wago-org/net/internal/plugintest" + wago "github.com/wago-org/wago" +) + +func loadNetwork(runtime *wago.Runtime, network *wagonet.Network) error { + return plugintest.LoadNetwork(runtime, network) +} + +func compileImportHarness(runtime *wago.Runtime) (*wago.Module, error) { + return plugintest.CompileImportHarness(runtime, wagonet.LinkLocal4Module) +} diff --git a/linklocal4/register/register.go b/linklocal4/register/register.go index d0e4b3c..50da7b9 100644 --- a/linklocal4/register/register.go +++ b/linklocal4/register/register.go @@ -1,4 +1,4 @@ -// Package register self-registers the selective IPv4 link-local-only Wago networking extension. +// Package register exposes the explicit IPv4 link-local networking catalog entry. package register import ( @@ -7,12 +7,15 @@ import ( wago "github.com/wago-org/wago" ) -func init() { - wago.RegisterExtension("net-linklocal4", func() wago.Extension { - network := wagonet.New() - if err := linklocal4.Register(network); err != nil { - panic("wagonet/linklocal4/register: " + err.Error()) - } - return network +func Provider() wago.PluginProvider { + return wagonet.Provider(wagonet.ProviderSpec{ + ID: "github.com/wago-org/net/linklocal4", Name: "Wago IPv4 Link-Local", Description: "Bounded checked IPv4 link-local networking.", + Modules: []string{wagonet.Module, wagonet.LinkLocal4Module}, + Factory: func() (*wagonet.Network, error) { + network := wagonet.New() + return network, linklocal4.Register(network) + }, }) } + +func Providers() []wago.PluginProvider { return []wago.PluginProvider{Provider()} } diff --git a/linklocal4/register/register_test.go b/linklocal4/register/register_test.go index 2a2206e..6d66583 100644 --- a/linklocal4/register/register_test.go +++ b/linklocal4/register/register_test.go @@ -1,21 +1,25 @@ package register_test import ( + "context" "reflect" "testing" wagonet "github.com/wago-org/net" - _ "github.com/wago-org/net/linklocal4/register" + "github.com/wago-org/net/internal/plugintest" + netregister "github.com/wago-org/net/linklocal4/register" wago "github.com/wago-org/wago" ) func TestLinkLocal4FactoryHasExactRuntimeSurface(t *testing.T) { - extension, ok := wago.NewExtension("net-linklocal4") - if !ok { - t.Fatal("link-local-only extension was not registered") + providers := netregister.Providers() + if len(providers) != 1 { + t.Fatalf("Providers = %d, want 1", len(providers)) } + provider := providers[0] runtime := wago.NewRuntime() - if err := runtime.Use(extension); err != nil { + defer runtime.Close() + if err := runtime.LoadPlugins(context.Background(), plugintest.Set(provider)); err != nil { t.Fatal(err) } if got, want := runtime.Capabilities(), []wago.Capability{wagonet.CapInfo, wagonet.CapLinkLocal4}; !reflect.DeepEqual(got, want) { diff --git a/linklocal4_runtime_test.go b/linklocal4_runtime_test.go index 18a35a6..7ba7b4f 100644 --- a/linklocal4_runtime_test.go +++ b/linklocal4_runtime_test.go @@ -69,7 +69,7 @@ func TestActualBackendGuestLinkLocal4SuccessfulFixedABILifecycle(t *testing.T) { } runtime := wago.NewRuntime() - if err := runtime.Use(extension); err != nil { + if err := loadNetwork(runtime, extension); err != nil { t.Fatal(err) } if got := runtime.Capabilities(); !reflect.DeepEqual(got, []wago.Capability{CapInfo, CapLinkLocal4}) { @@ -80,7 +80,7 @@ func TestActualBackendGuestLinkLocal4SuccessfulFixedABILifecycle(t *testing.T) { t.Fatalf("unselected protocol import leaked: %s.%s", spec.Module, spec.Name) } } - compiled, err := runtime.Compile([]byte{0x00, 0x61, 0x73, 0x6d, 0x01, 0, 0, 0}) + compiled, err := compileImportHarness(runtime, LinkLocal4Module) if err != nil { t.Fatal(err) } @@ -88,8 +88,9 @@ func TestActualBackendGuestLinkLocal4SuccessfulFixedABILifecycle(t *testing.T) { if err != nil { t.Fatal(err) } - host := runtimeLinkLocalHost{instance: instance, memory: bytes.Repeat([]byte{0xa5}, 512)} - state, ok := extension.instanceManager().ForInstance(instance) + host := runtimeLinkLocalHost{instance: instance, memory: instance.Memory().Bytes()} + copy(host.memory[:512], bytes.Repeat([]byte{0xa5}, 512)) + state, ok := extension.instanceManager().SingleState() if !ok { t.Fatal("missing actual link-local state") } @@ -114,7 +115,8 @@ func TestActualBackendGuestLinkLocal4SuccessfulFixedABILifecycle(t *testing.T) { } beforeInvalid := append([]byte(nil), host.memory...) - if got := callRuntimeLinkLocal(t, runtime, host, "result", uint64(claim), 480); got != StatusInvalidArgument || !bytes.Equal(host.memory, beforeInvalid) { + shortResult := uint64(len(host.memory) - 32) + if got := callRuntimeLinkLocal(t, runtime, host, "result", uint64(claim), shortResult); got != StatusInvalidArgument || !bytes.Equal(host.memory, beforeInvalid) { t.Fatalf("short result = %v mutated=%v", got, !bytes.Equal(host.memory, beforeInvalid)) } @@ -203,7 +205,7 @@ func TestActualBackendGuestLinkLocal4SuccessfulFixedABILifecycle(t *testing.T) { if err := instance.Close(); err != nil { t.Fatal(err) } - if _, exists := extension.instanceManager().ForInstance(instance); exists { + if extension.instanceManager().Len() != 0 { t.Fatal("link-local state survived instance close") } if usage, closed := account.Snapshot(); !closed || usage != (quota.Usage{}) { @@ -222,11 +224,13 @@ func runtimeLinkLocalIPv4Address(namespace *lnetocore.Namespace) netip.Addr { func callRuntimeLinkLocal(t testing.TB, runtime *wago.Runtime, host runtimeLinkLocalHost, name string, params ...uint64) Status { t.Helper() - function, ok := runtime.HostImports()[linklocalbinding.Module+"."+name].(wago.HostFunc) - if !ok { - t.Fatalf("link-local import %q missing", name) + _ = runtime + results, err := host.instance.Invoke(name, params...) + if err != nil { + t.Fatalf("link-local import %q: %v", name, err) + } + if len(results) != 1 { + t.Fatalf("link-local import %q results = %d", name, len(results)) } - var results [1]uint64 - function(host, params, results[:]) return Status(int32(results[0])) } diff --git a/mdns/mdns_test.go b/mdns/mdns_test.go index 6e898f5..9a74b19 100644 --- a/mdns/mdns_test.go +++ b/mdns/mdns_test.go @@ -9,7 +9,6 @@ import ( wagonet "github.com/wago-org/net" mdnsabi "github.com/wago-org/net/internal/abi/mdns" - mdnsbinding "github.com/wago-org/net/internal/binding/mdns" "github.com/wago-org/net/internal/guest" mdnsns "github.com/wago-org/net/internal/namespace/mdns" "github.com/wago-org/net/internal/resource" @@ -41,7 +40,7 @@ func TestSelectiveMDNSRegistrationAndActualBackendLifecycle(t *testing.T) { t.Fatal(err) } runtime := wago.NewRuntime() - if err := runtime.Use(network); err != nil { + if err := loadNetwork(runtime, network); err != nil { t.Fatal(err) } imports := 0 @@ -56,7 +55,7 @@ func TestSelectiveMDNSRegistrationAndActualBackendLifecycle(t *testing.T) { if imports != 10 { t.Fatalf("mDNS imports = %d, want 10", imports) } - module, err := runtime.Compile([]byte{0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00}) + module, err := compileImportHarness(runtime) if err != nil { t.Fatal(err) } @@ -65,7 +64,7 @@ func TestSelectiveMDNSRegistrationAndActualBackendLifecycle(t *testing.T) { t.Fatal(err) } defer instance.Close() - host := hostModule{instance: instance, memory: make([]byte, 2048)} + host := hostModule{instance: instance, memory: instance.Memory().Bytes()} if got := callImport(t, runtime, host, "namespace_default", 1500); got != guest.StatusOK { t.Fatalf("namespace_default = %v", got) } @@ -146,16 +145,16 @@ func TestMDNSDenyWinsAndOptionsAreFinite(t *testing.T) { t.Fatal(err) } runtime := wago.NewRuntime() - if err := runtime.Use(network); err != nil { + if err := loadNetwork(runtime, network); err != nil { t.Fatal(err) } - module, _ := runtime.Compile([]byte{0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00}) + module, _ := compileImportHarness(runtime) instance, err := runtime.Instantiate(context.Background(), module) if err != nil { t.Fatal(err) } defer instance.Close() - host := hostModule{instance: instance, memory: make([]byte, 512)} + host := hostModule{instance: instance, memory: instance.Memory().Bytes()} if got := callImport(t, runtime, host, "namespace_default", 400); got != guest.StatusOK { t.Fatal(got) } @@ -165,18 +164,16 @@ func TestMDNSDenyWinsAndOptionsAreFinite(t *testing.T) { t.Fatalf("deny-wins query = %v", got) } before := append([]byte(nil), host.memory...) - if got := callImport(t, runtime, host, "query", namespace, 0, 508); got != guest.StatusInvalidArgument || !bytes.Equal(before, host.memory) { + if got := callImport(t, runtime, host, "query", namespace, 0, uint64(len(host.memory)-4)); got != guest.StatusInvalidArgument || !bytes.Equal(before, host.memory) { t.Fatalf("invalid output = %v mutated=%v", got, !bytes.Equal(before, host.memory)) } } -func callImport(t testing.TB, runtime *wago.Runtime, host hostModule, name string, params ...uint64) guest.Status { +func callImport(t testing.TB, _ *wago.Runtime, host hostModule, name string, params ...uint64) guest.Status { t.Helper() - function, ok := runtime.HostImports()[mdnsbinding.Module+"."+name].(wago.HostFunc) - if !ok { - t.Fatalf("mDNS import %q missing", name) + results, err := host.instance.Invoke(name, params...) + if err != nil || len(results) != 1 { + t.Fatalf("mDNS import %q = %v, %v", name, results, err) } - var results [1]uint64 - function(host, params, results[:]) return guest.Status(int32(results[0])) } diff --git a/mdns/plugin_test.go b/mdns/plugin_test.go new file mode 100644 index 0000000..90b222b --- /dev/null +++ b/mdns/plugin_test.go @@ -0,0 +1,15 @@ +package mdns + +import ( + wagonet "github.com/wago-org/net" + "github.com/wago-org/net/internal/plugintest" + wago "github.com/wago-org/wago" +) + +func loadNetwork(runtime *wago.Runtime, network *wagonet.Network) error { + return plugintest.LoadNetwork(runtime, network) +} + +func compileImportHarness(runtime *wago.Runtime) (*wago.Module, error) { + return plugintest.CompileImportHarness(runtime, wagonet.MDNSModule) +} diff --git a/mdns/register/register.go b/mdns/register/register.go index 227fe69..b531984 100644 --- a/mdns/register/register.go +++ b/mdns/register/register.go @@ -1,4 +1,4 @@ -// Package register self-registers the selective mDNS-only Wago networking extension. +// Package register exposes the explicit mDNS-only networking catalog entry. package register import ( @@ -7,12 +7,15 @@ import ( wago "github.com/wago-org/wago" ) -func init() { - wago.RegisterExtension("net-mdns", func() wago.Extension { - network := wagonet.New() - if err := mdns.Register(network); err != nil { - panic("wagonet/mdns/register: " + err.Error()) - } - return network +func Provider() wago.PluginProvider { + return wagonet.Provider(wagonet.ProviderSpec{ + ID: "github.com/wago-org/net/mdns", Name: "Wago mDNS", Description: "Bounded checked multicast DNS networking.", + Modules: []string{wagonet.Module, wagonet.MDNSModule}, + Factory: func() (*wagonet.Network, error) { + network := wagonet.New() + return network, mdns.Register(network) + }, }) } + +func Providers() []wago.PluginProvider { return []wago.PluginProvider{Provider()} } diff --git a/mdns/register/register_test.go b/mdns/register/register_test.go index a374ced..6716d73 100644 --- a/mdns/register/register_test.go +++ b/mdns/register/register_test.go @@ -1,21 +1,25 @@ package register_test import ( + "context" "reflect" "testing" wagonet "github.com/wago-org/net" - _ "github.com/wago-org/net/mdns/register" + "github.com/wago-org/net/internal/plugintest" + netregister "github.com/wago-org/net/mdns/register" wago "github.com/wago-org/wago" ) func TestMDNSFactoryHasExactRuntimeSurface(t *testing.T) { - extension, ok := wago.NewExtension("net-mdns") - if !ok { - t.Fatal("net-mdns plugin was not registered") + providers := netregister.Providers() + if len(providers) != 1 { + t.Fatalf("Providers = %d, want 1", len(providers)) } + provider := providers[0] runtime := wago.NewRuntime() - if err := runtime.Use(extension); err != nil { + defer runtime.Close() + if err := runtime.LoadPlugins(context.Background(), plugintest.Set(provider)); err != nil { t.Fatal(err) } if got, want := runtime.Capabilities(), []wago.Capability{wagonet.CapInfo, wagonet.CapMDNS}; !reflect.DeepEqual(got, want) { diff --git a/net.go b/net.go index 9b5abc5..b93a72f 100644 --- a/net.go +++ b/net.go @@ -9,9 +9,12 @@ package net import ( - "embed" + "bytes" "encoding/json" + "fmt" + "io" "net/netip" + "sort" "sync" lnetocore "github.com/wago-org/net/internal/backend/lneto/core" @@ -23,8 +26,161 @@ import ( "github.com/wago-org/net/internal/quota" "github.com/wago-org/net/internal/readiness" wago "github.com/wago-org/wago" + wagoplugin "github.com/wago-org/wago/plugin" ) +const PluginID = "github.com/wago-org/net" + +// Service is the major-versioned cross-plugin networking seam. Implementations +// expose immutable topology and exact-caller readiness without leaking Runtime, +// instance, namespace, or resource ownership. +type Service interface { + ImportModules() []string + Ready(wago.HostModule) bool +} + +// Contract is the typed v1 networking composition seam. +var Contract = wagoplugin.NewContract[Service](PluginID+"/service", 1) + +var emptyConfigSchema = json.RawMessage(`{ + "type": "object", + "additionalProperties": false +}`) + +// ProviderSpec describes one explicitly linked networking composition. Modules +// is the exact Wasm host-import authority scope; Factory must return a fresh, +// fully configured protocol composition. +type ProviderSpec struct { + ID string + Name string + Description string + Modules []string + Factory func() (*Network, error) +} + +// Definition returns immutable metadata for one networking composition. +func Definition(spec ProviderSpec) wago.PluginDefinition { + modules := append([]string(nil), spec.Modules...) + sort.Strings(modules) + return wago.PluginDefinition{ + ID: spec.ID, Name: spec.Name, Version: "0.1.0", Description: spec.Description, + Stability: wago.Experimental, + Compatibility: wago.Compatibility{Engines: map[string]string{"wago": ">=0.1.0"}}, + Provenance: wago.PluginProvenance{ + Homepage: "https://github.com/wago-org/net#readme", Repository: "https://github.com/wago-org/net", License: "Apache-2.0", + Authors: []string{"Wago contributors"}, + }, + Authorities: []wago.AuthorityRequest{ + {Name: wago.AuthorityHostImportDefine, Mode: wago.AuthorityRequired, Reason: "define the selected checked networking guest imports", Scope: wago.AuthorityScope{Modules: modules}}, + {Name: wago.AuthorityHostCallerIdentify, Mode: wago.AuthorityRequired, Reason: "resolve exact synchronous callers without instance authority"}, + {Name: wago.AuthorityInstanceInstantiateIntercept, Mode: wago.AuthorityRequired, Reason: "transactionally attach isolated network state before guest start"}, + {Name: wago.AuthorityInstanceCloseObserve, Mode: wago.AuthorityRequired, Reason: "detach network state before exact instance resources close"}, + }, + ConfigSchema: append(json.RawMessage(nil), emptyConfigSchema...), + Provides: []wago.ContractSpec{Contract.Spec()}, + } +} + +// Provider is a side-effect-free catalog entry for an exact composition. +func Provider(spec ProviderSpec) wago.PluginProvider { + modules := append([]string(nil), spec.Modules...) + sort.Strings(modules) + definitionSpec := spec + definitionSpec.Modules = modules + return wago.PluginProvider{ + Definition: Definition(definitionSpec), + New: func() wago.Plugin { + return &providerPlugin{factory: spec.Factory, modules: append([]string(nil), modules...)} + }, + ValidateConfig: validateEmptyPluginConfig, + } +} + +type providerPlugin struct { + factory func() (*Network, error) + modules []string +} + +func (p *providerPlugin) Register(reg *wago.Registrar) error { + if p == nil || p.factory == nil { + return fmt.Errorf("wagonet: nil provider factory") + } + var config struct{} + if err := reg.Config(&config); err != nil { + return err + } + network, err := p.factory() + if err != nil { + return err + } + if network == nil { + return instancestate.ErrInvalidConfig + } + return network.registerPlugin(reg, p.modules) +} + +func validateEmptyPluginConfig(raw json.RawMessage) error { + if len(raw) == 0 { + raw = json.RawMessage(`{}`) + } + if err := validateConfigObject(raw); err != nil { + return fmt.Errorf("wagonet: config: %w", err) + } + var config struct{} + decoder := json.NewDecoder(bytes.NewReader(raw)) + decoder.DisallowUnknownFields() + if err := decoder.Decode(&config); err != nil { + return fmt.Errorf("wagonet: config: %w", err) + } + if err := decoder.Decode(new(any)); err != io.EOF { + return fmt.Errorf("wagonet: config has a trailing JSON value") + } + return nil +} + +func validateConfigObject(raw json.RawMessage) error { + decoder := json.NewDecoder(bytes.NewReader(raw)) + token, err := decoder.Token() + if err != nil { + return err + } + if token != json.Delim('{') { + return fmt.Errorf("must be a JSON object") + } + seen := map[string]struct{}{} + for decoder.More() { + keyToken, err := decoder.Token() + if err != nil { + return err + } + key, ok := keyToken.(string) + if !ok { + return fmt.Errorf("object key is not a string") + } + if _, duplicate := seen[key]; duplicate { + return fmt.Errorf("duplicate field %q", key) + } + seen[key] = struct{}{} + var value json.RawMessage + if err := decoder.Decode(&value); err != nil { + return err + } + if bytes.Equal(bytes.TrimSpace(value), []byte("null")) { + return fmt.Errorf("field %q must not be null", key) + } + } + if _, err := decoder.Token(); err != nil { + return err + } + if err := decoder.Decode(new(any)); err != io.EOF { + if err == nil { + return fmt.Errorf("has a trailing JSON value") + } + return err + } + return nil +} + const ( // Module is the core networking WebAssembly import module. Module = "wago_net" @@ -117,7 +273,7 @@ const ( // QuotaLimits and ReadinessConfig are finite per-instance limits. Zero values // deny the corresponding class; pointers in Config distinguish explicit zero -// limits from the extension defaults. +// limits from the network defaults. type QuotaLimits = quota.Limits type ReadinessConfig = readiness.Config @@ -178,7 +334,7 @@ type StaticIPv4Config struct { } // Config configures immutable authority and finite instance-owned networking -// state. A nil StaticIPv4 leaves the extension state-only and guest-visible +// state. A nil StaticIPv4 leaves the network state-only and guest-visible // inspection remains unchanged. type Config struct { Policy PolicyConfig @@ -218,9 +374,8 @@ func WithConfig(config Config) Option { }) } -// Extension implements the shared Wago networking composition and lifecycle -// layer. Network is its selective-API name. -type Extension struct { +// Network is the shared Wago networking composition and lifecycle builder. +type Network struct { config Config configErr error modules plugin.Set @@ -230,9 +385,6 @@ type Extension struct { stateErr error } -// Network is the shared builder passed to protocol registration packages. -type Network = Extension - // New constructs an initially protocol-free network. Protocol packages select // their exact capability and import surface before the network is passed to // Wago. Registration freezes on the first Wago Register call. @@ -240,17 +392,17 @@ func New(options ...Option) *Network { var config Config for _, option := range options { if option == nil { - return &Extension{configErr: instancestate.ErrInvalidConfig} + return &Network{configErr: instancestate.ErrInvalidConfig} } if err := option.applyNetwork(&config); err != nil { - return &Extension{config: config, configErr: err} + return &Network{config: config, configErr: err} } } - return newExtension(config) + return newNetwork(config) } -func newExtension(config Config) *Extension { - return &Extension{config: cloneConfig(config)} +func newNetwork(config Config) *Network { + return &Network{config: cloneConfig(config)} } func cloneConfig(config Config) Config { @@ -271,7 +423,7 @@ func cloneConfig(config Config) Config { return cloned } -func (e *Extension) initialize(modules []plugin.Module) (*instancestate.Manager, error) { +func (e *Network) initialize(modules []plugin.Module) (*instancestate.Manager, error) { if e == nil { return nil, instancestate.ErrInvalidConfig } @@ -284,7 +436,7 @@ func (e *Extension) initialize(modules []plugin.Module) (*instancestate.Manager, return e.instances, e.stateErr } -func (e *Extension) buildManager(modules []plugin.Module) (*instancestate.Manager, error) { +func (e *Network) buildManager(modules []plugin.Module) (*instancestate.Manager, error) { managerConfig := instancestate.DefaultConfig() managerConfig.Policy = policy.Merge(e.config.Policy) for _, module := range modules { @@ -345,18 +497,16 @@ func installNamespaceServices(common nscore.Namespace, modules []plugin.Module) // RegisterModule installs one opaque protocol descriptor. The internal type in // this signature deliberately limits direct use to this module's public // protocol packages. -func (e *Extension) RegisterModule(module plugin.Module) error { +func (e *Network) RegisterModule(module plugin.Module) error { if e == nil { return instancestate.ErrInvalidConfig } return e.modules.Add(module) } -// Info returns extension metadata loaded from wago.json. -func (e *Extension) Info() wago.ExtensionInfo { return cloneExtensionInfo(extensionInfo) } - -// Register declares the core networking capability and host imports. -func (e *Extension) Register(reg *wago.Registry) error { +// registerPlugin declares the exact networking imports and lifecycle owned by +// one explicit provider. +func (e *Network) registerPlugin(reg *wago.Registrar, declaredModules []string) error { if e == nil || e.configErr != nil { if e == nil { return instancestate.ErrInvalidConfig @@ -365,31 +515,88 @@ func (e *Extension) Register(reg *wago.Registry) error { } modules := e.modules.Freeze() if len(modules) == 0 { - return nil + return fmt.Errorf("wagonet: provider selected no protocol modules") } instances, err := e.initialize(modules) if err != nil { return err } - reg.RequireReinstantiation() - reg.Hooks().AfterInstantiate(instances.AfterInstantiate) - reg.Hooks().BeforeClose(instances.BeforeClose) - reg.Capability(CapInfo, wago.CapabilityDocs("inspect the Wago networking ABI and interfaces")) - registerBindings(reg.ImportModule(Module), e.bindings()) - host := plugin.NewHost(instances) + imports, err := reg.HostImports() + if err != nil { + return err + } + callers, err := reg.HostCallers() + if err != nil { + return err + } + instantiate, err := reg.InstanceInstantiateInterceptor() + if err != nil { + return err + } + if err := instantiate.After(func(event wago.InstantiationEvent) error { + return instances.AttachIdentity(event.Instance) + }); err != nil { + return err + } + closeObserver, err := reg.InstanceCloseObserver() + if err != nil { + return err + } + if err := closeObserver.Before(func(event wago.InstanceCloseEvent) { + _ = instances.DetachIdentity(event.Instance) + }); err != nil { + return err + } + if err := reg.GuestCapability(CapInfo, wago.CapabilityDocs("inspect the Wago networking ABI and interfaces")); err != nil { + return err + } + coreModule, err := imports.Module(Module) + if err != nil { + return err + } + registerBindings(coreModule, e.bindings()) + host := plugin.NewRuntimeHost(instances, callers) for _, module := range modules { - module.Install(reg, host) + if err := module.Install(reg, imports, host); err != nil { + return err + } } - return nil + service := &networkService{modules: append([]string(nil), declaredModules...), instances: instances, callers: callers} + return wagoplugin.Provide(reg, Contract, Service(service)) +} + +type networkService struct { + modules []string + instances *instancestate.Manager + callers *wago.CallerResolver +} + +func (s *networkService) ImportModules() []string { + if s == nil { + return nil + } + return append([]string(nil), s.modules...) +} + +func (s *networkService) Ready(caller wago.HostModule) bool { + if s == nil || s.instances == nil || s.callers == nil { + return false + } + identity, err := s.callers.Resolve(caller) + if err != nil { + return false + } + _, ok := s.instances.ForIdentity(identity) + return ok } // InfoImports returns the explicit stateless core host imports for Wago's // low-level Instantiate path. It is limited to shared inspection helpers such // as abi_version; resource-owning protocol imports require the Runtime -// extension path so per-instance lifecycle state can be attached and cleaned. +// provider path so per-instance lifecycle state can be attached and cleaned. func InfoImports() wago.Imports { imports := make(wago.Imports) - for _, binding := range newExtension(Config{}).bindings() { + for _, binding := range newNetwork(Config{}).bindings() { imports[Module+"."+binding.name] = binding.fn } return imports @@ -398,7 +605,7 @@ func InfoImports() wago.Imports { // Imports preserves the historical low-level helper surface. Only the zero // configuration is accepted because low-level imports cannot own configured // protocol resources or lifecycle state; configured callers must fail closed -// and use the Runtime extension path instead. +// and use the Runtime provider path instead. func Imports(config Config) wago.Imports { if !lowLevelImportsAllowed(config) { return nil @@ -439,7 +646,7 @@ type binding struct { docs string } -func (e *Extension) bindings() []binding { +func (e *Network) bindings() []binding { return []binding{ { name: "abi_version", @@ -458,7 +665,7 @@ func abiVersion(_ wago.HostModule, params, results []uint64) { results[0] = uint64(ABIVersion1) } -func (e *Extension) instanceManager() *instancestate.Manager { +func (e *Network) instanceManager() *instancestate.Manager { if e == nil || e.configErr != nil { return nil } @@ -480,66 +687,3 @@ func lnetoCoreConfig(config StaticIPv4Config) lnetocore.Config { Link: config.Link, } } - -type manifest struct { - Module string `json:"module"` - Name string `json:"name"` - Version string `json:"version"` - Description string `json:"description"` - Stability string `json:"stability"` - License string `json:"license"` - Homepage string `json:"homepage"` - Repository string `json:"repository"` - Authors []string `json:"authors"` - Keywords []string `json:"keywords"` - Engines map[string]string `json:"engines"` - Platforms []string `json:"platforms"` - Private bool `json:"private"` -} - -//go:embed wago.json -var manifestFiles embed.FS - -var extensionInfo = loadExtensionInfo() - -func loadExtensionInfo() wago.ExtensionInfo { - data, err := manifestFiles.ReadFile("wago.json") - if err != nil { - panic("wagonet: reading wago.json: " + err.Error()) - } - var m manifest - if err := json.Unmarshal(data, &m); err != nil { - panic("wagonet: parsing wago.json: " + err.Error()) - } - return cloneExtensionInfo(wago.ExtensionInfo{ - ID: m.Module, - Name: m.Name, - Version: m.Version, - Description: m.Description, - Stability: wago.Stability(m.Stability), - License: m.License, - Homepage: m.Homepage, - Repository: m.Repository, - Authors: m.Authors, - Tags: m.Keywords, - Private: m.Private, - Compat: wago.Compatibility{ - Engines: m.Engines, - Platforms: m.Platforms, - }, - }) -} - -func cloneExtensionInfo(info wago.ExtensionInfo) wago.ExtensionInfo { - cloned := info - cloned.Authors = append([]string(nil), info.Authors...) - cloned.Tags = append([]string(nil), info.Tags...) - cloned.Compat.Platforms = append([]string(nil), info.Compat.Platforms...) - if info.Compat.Engines != nil { - cloned.Compat.Engines = make(map[string]string, len(info.Compat.Engines)) - for key, value := range info.Compat.Engines { - cloned.Compat.Engines[key] = value - } - } - return cloned -} diff --git a/net_test.go b/net_test.go index c9fc8b9..284f78c 100644 --- a/net_test.go +++ b/net_test.go @@ -20,23 +20,16 @@ import ( func TestExtensionMetadataAndABIBinding(t *testing.T) { ext := Init(Config{}) - info := ext.Info() - if info.ID != "github.com/wago-org/net" || info.Stability != wago.Experimental { - t.Fatalf("Info = %+v", info) + definition := Definition(ProviderSpec{ID: PluginID, Name: "Wago Networking", Description: "test", Modules: []string{Module, UDPModule, TCPModule, DNSModule}}) + if definition.ID != PluginID || definition.Stability != wago.Experimental { + t.Fatalf("Definition = %+v", definition) } - if got := info.Compat.Engines["wago"]; got != "0.1.0" { + if got := definition.Compatibility.Engines["wago"]; got != ">=0.1.0" { t.Fatalf("wago compatibility = %q", got) } - if !info.Private { - t.Fatal("experimental exact-revision plugin must remain private") - } - wantTags := []string{"networking", "tcp", "udp", "dns", "capability-gated", "wago", "lneto", "wasm"} - if !reflect.DeepEqual(info.Tags, wantTags) { - t.Fatalf("Info tags = %v, want %v", info.Tags, wantTags) - } rt := wago.NewRuntime() - if err := rt.Use(ext); err != nil { + if err := loadNetwork(rt, ext); err != nil { t.Fatalf("Use: %v", err) } if got := rt.Capabilities(); !reflect.DeepEqual(got, []wago.Capability{CapDNS, CapInfo, CapTCP, CapUDP}) { @@ -109,21 +102,22 @@ func TestExtensionMetadataAndABIBinding(t *testing.T) { t.Fatalf("missing protocol imports: DNS=%v UDP=%v TCP=%v", wantDNS, wantUDP, wantTCP) } - fn, ok := rt.HostImports()[Module+".abi_version"].(wago.HostFunc) - if !ok { - t.Fatalf("abi_version binding has type %T", rt.HostImports()[Module+".abi_version"]) + module, err := compileImportHarness(rt, Module) + if err != nil { + t.Fatal(err) + } + instance, err := rt.Instantiate(context.Background(), module) + if err != nil { + t.Fatal(err) + } + defer instance.Close() + results, err := instance.Invoke("abi_version") + if err != nil || len(results) != 1 { + t.Fatalf("abi_version invoke = %v, %v", results, err) } - results := make([]uint64, 1) - fn(nil, nil, results) if got := uint32(results[0]); got != ABIVersion1 { t.Fatalf("abi_version = %#x, want %#x", got, ABIVersion1) } - fn(nil, nil, nil) - malformed := []uint64{0xfeedface} - fn(nil, []uint64{1}, malformed) - if malformed[0] != 0xfeedface { - t.Fatalf("malformed abi_version call mutated result: %#x", malformed[0]) - } } func TestInfoImportsStayCoreOnlyAndRejectConfiguredState(t *testing.T) { @@ -154,81 +148,14 @@ func TestInfoImportsStayCoreOnlyAndRejectConfiguredState(t *testing.T) { } } -func TestExtensionInfoReturnsIndependentMutableCopies(t *testing.T) { - ext := Init(Config{}) - first := ext.Info() - if len(first.Authors) == 0 || len(first.Tags) == 0 || len(first.Compat.Engines) == 0 { - t.Fatalf("unexpected extension info = %+v", first) - } - first.Authors[0] = "mutated author" - first.Tags[0] = "mutated-tag" - if len(first.Compat.Platforms) != 0 { - first.Compat.Platforms = append(first.Compat.Platforms[:0:0], "mutated-platform") - } - first.Compat.Engines["wago"] = "mutated-engine" - - second := ext.Info() - if second.Authors[0] == "mutated author" || second.Tags[0] == "mutated-tag" || second.Compat.Engines["wago"] == "mutated-engine" { - t.Fatalf("Info returned aliased mutable data: %+v", second) - } -} - -func TestExtensionInfoConcurrentCallsDoNotAlias(t *testing.T) { - ext := Init(Config{}) - infos := make([]wago.ExtensionInfo, 2) - start := make(chan struct{}) - var workers sync.WaitGroup - for i := range infos { - workers.Add(1) - go func(i int) { - defer workers.Done() - <-start - infos[i] = ext.Info() - }(i) - } - close(start) - workers.Wait() - if len(infos[0].Authors) == 0 || len(infos[1].Authors) == 0 || len(infos[0].Compat.Engines) == 0 || len(infos[1].Compat.Engines) == 0 { - t.Fatalf("unexpected concurrent Info results: %+v / %+v", infos[0], infos[1]) - } - infos[0].Authors[0] = "mutated concurrently" - infos[0].Compat.Engines["wago"] = "mutated concurrently" - if infos[1].Authors[0] == "mutated concurrently" || infos[1].Compat.Engines["wago"] == "mutated concurrently" { - t.Fatalf("concurrent Info calls aliased mutable data: %+v / %+v", infos[0], infos[1]) - } -} - -func TestEmptyNetworkLeavesLifecycleAndResetPolicyUnchanged(t *testing.T) { - network := New() - runtime := wago.NewRuntime() - if err := runtime.Use(network); err != nil { - t.Fatalf("Use: %v", err) - } - if len(runtime.Capabilities()) != 0 || len(runtime.ProvidedImports()) != 0 { - t.Fatalf("empty network exported capabilities/imports: %v / %v", runtime.Capabilities(), runtime.ProvidedImports()) - } - if network.instances != nil { - t.Fatal("empty network initialized instance state during registration") - } - class, err := runtime.Class(emptyModule(t, runtime), wago.ClassOptions{ - Pool: wago.PoolOptions{MinInstances: 1, MaxInstances: 1, Reset: wago.ResetMemorySnapshot}, - }) - if err != nil { - t.Fatalf("Class: %v", err) - } - if got := class.ResetPolicy(); got != wago.ResetMemorySnapshot { - t.Fatalf("empty network reset policy = %v, want memory snapshot", got) - } - lease, err := class.Acquire(context.Background()) - if err != nil { - t.Fatalf("Acquire: %v", err) - } - if network.instances != nil { - _ = lease.Release() - t.Fatal("empty network attached instance state") - } - if err := lease.Release(); err != nil { - t.Fatalf("Release: %v", err) +func TestDefinitionReturnsIndependentMutableCopies(t *testing.T) { + spec := ProviderSpec{ID: PluginID, Name: "Wago Networking", Description: "test", Modules: []string{Module, UDPModule}} + first := Definition(spec) + first.Compatibility.Engines["wago"] = "mutated" + first.Authorities[0].Scope.Modules[0] = "mutated" + second := Definition(spec) + if second.Compatibility.Engines["wago"] == "mutated" || second.Authorities[0].Scope.Modules[0] == "mutated" { + t.Fatalf("Definition returned aliased metadata: %+v", second) } } @@ -249,7 +176,7 @@ func TestExtensionSnapshotsCallerConfigBeforeRegistrationAndInstantiation(t *tes StaticIPv4: staticIPv4, } extension := New(WithConfig(config)) - if err := extension.RegisterModule(plugin.NewModule(plugin.ModuleUDP, func(*wago.Registry, plugin.Host) {})); err != nil { + if err := extension.RegisterModule(plugin.NewModule(plugin.ModuleUDP, func(*plugin.Registrar, plugin.Host) {})); err != nil { t.Fatalf("Register module: %v", err) } @@ -261,7 +188,7 @@ func TestExtensionSnapshotsCallerConfigBeforeRegistrationAndInstantiation(t *tes staticIPv4.IPv4Address = netip.Addr{} runtime := wago.NewRuntime() - if err := runtime.Use(extension); err != nil { + if err := loadNetwork(runtime, extension); err != nil { t.Fatalf("Use: %v", err) } instance, err := runtime.Instantiate(context.Background(), emptyModule(t, runtime)) @@ -269,7 +196,7 @@ func TestExtensionSnapshotsCallerConfigBeforeRegistrationAndInstantiation(t *tes t.Fatalf("Instantiate: %v", err) } defer instance.Close() - state, ok := extension.instanceManager().ForInstance(instance) + state, ok := extension.instanceManager().SingleState() if !ok || state == nil { t.Fatal("instance state missing") } @@ -303,7 +230,7 @@ func TestExtensionConfigSnapshotIsRaceSafeAgainstCallerMutation(t *testing.T) { StaticIPv4: staticIPv4, } extension := New(WithConfig(config)) - if err := extension.RegisterModule(plugin.NewModule(plugin.ModuleUDP, func(*wago.Registry, plugin.Host) {})); err != nil { + if err := extension.RegisterModule(plugin.NewModule(plugin.ModuleUDP, func(*plugin.Registrar, plugin.Host) {})); err != nil { t.Fatalf("Register module: %v", err) } @@ -339,7 +266,7 @@ func TestExtensionConfigSnapshotIsRaceSafeAgainstCallerMutation(t *testing.T) { }() runtime := wago.NewRuntime() - if err := runtime.Use(extension); err != nil { + if err := loadNetwork(runtime, extension); err != nil { t.Fatalf("Use: %v", err) } module := emptyModule(t, runtime) @@ -369,7 +296,7 @@ func TestProtocolConfigurationAdvertisesCompleteGuestSurface(t *testing.T) { }, }}) runtime := wago.NewRuntime() - if err := runtime.Use(extension); err != nil { + if err := loadNetwork(runtime, extension); err != nil { t.Fatalf("Use TCP-configured extension: %v", err) } if got := runtime.Capabilities(); !reflect.DeepEqual(got, []wago.Capability{CapDNS, CapInfo, CapTCP, CapUDP}) { @@ -394,10 +321,10 @@ func TestSelectiveBackendAssemblyRejectsIncompatibleFamily(t *testing.T) { backend := plugin.NewBackend("other", nil, func(any) (nscore.Service, error) { return nscore.Service{Key: "test", Value: new(int)}, nil }) - if err := extension.RegisterModule(plugin.NewModule(plugin.ModuleTCP, func(*wago.Registry, plugin.Host) {}, backend)); err != nil { + if err := extension.RegisterModule(plugin.NewModule(plugin.ModuleTCP, func(*plugin.Registrar, plugin.Host) {}, backend)); err != nil { t.Fatal(err) } - if err := wago.NewRuntime().Use(extension); !errors.Is(err, plugin.ErrIncompatibleBackend) { + if err := loadNetwork(wago.NewRuntime(), extension); !errors.Is(err, plugin.ErrIncompatibleBackend) { t.Fatalf("Use incompatible backend = %v", err) } if extension.instanceManager() != nil { @@ -445,15 +372,15 @@ func TestSelectiveBackendAssemblyConfiguresAndInstallsExactLnetoBase(t *testing. installOrder = append(installOrder, "second") return nscore.Service{Key: "second", Value: secondService}, nil }) - if err := extension.RegisterModule(plugin.NewModule(plugin.ModuleTCP, func(*wago.Registry, plugin.Host) {}, first)); err != nil { + if err := extension.RegisterModule(plugin.NewModule(plugin.ModuleTCP, func(*plugin.Registrar, plugin.Host) {}, first)); err != nil { t.Fatal(err) } - if err := extension.RegisterModule(plugin.NewModule(plugin.ModuleIPv6, func(*wago.Registry, plugin.Host) {}, second)); err != nil { + if err := extension.RegisterModule(plugin.NewModule(plugin.ModuleIPv6, func(*plugin.Registrar, plugin.Host) {}, second)); err != nil { t.Fatal(err) } runtime := wago.NewRuntime() - if err := runtime.Use(extension); err != nil { + if err := loadNetwork(runtime, extension); err != nil { t.Fatalf("Use: %v", err) } if !reflect.DeepEqual(configureOrder, []string{"first", "second"}) || len(installOrder) != 0 { @@ -463,7 +390,7 @@ func TestSelectiveBackendAssemblyConfiguresAndInstallsExactLnetoBase(t *testing. if err != nil { t.Fatalf("Instantiate: %v", err) } - state, ok := extension.instanceManager().ForInstance(instance) + state, ok := extension.instanceManager().SingleState() if !ok || state == nil { t.Fatal("instance state missing") } @@ -513,11 +440,11 @@ func TestSelectiveBackendAssemblyRollsBackCoreBeforePublication(t *testing.T) { common, _ = base.(*lnetocore.Namespace) return nscore.Service{}, installErr }) - if err := extension.RegisterModule(plugin.NewModule(plugin.ModuleTCP, func(*wago.Registry, plugin.Host) {}, backend)); err != nil { + if err := extension.RegisterModule(plugin.NewModule(plugin.ModuleTCP, func(*plugin.Registrar, plugin.Host) {}, backend)); err != nil { t.Fatal(err) } runtime := wago.NewRuntime() - if err := runtime.Use(extension); err != nil { + if err := loadNetwork(runtime, extension); err != nil { t.Fatal(err) } if _, err := runtime.Instantiate(context.Background(), emptyModule(t, runtime)); !errors.Is(err, installErr) { @@ -584,7 +511,7 @@ func testBackendModule(key plugin.ModuleKey, service nscore.Service) plugin.Modu backend := plugin.NewBackend(plugin.BackendLnetoV1, nil, func(any) (nscore.Service, error) { return service, nil }) - return plugin.NewModule(key, func(*wago.Registry, plugin.Host) {}, backend) + return plugin.NewModule(key, func(*plugin.Registrar, plugin.Host) {}, backend) } func selectiveTestStaticIPv4() *StaticIPv4Config { @@ -599,7 +526,7 @@ func selectiveTestStaticIPv4() *StaticIPv4Config { func TestExtensionRejectsInvalidStaticNamespaceBeforeRegistration(t *testing.T) { extension := Init(Config{StaticIPv4: &StaticIPv4Config{}}) runtime := wago.NewRuntime() - err := runtime.Use(extension) + err := loadNetwork(runtime, extension) failure, ok := namespace.FailureOf(err) if !ok || failure != namespace.FailureInvalidArgument { t.Fatalf("invalid static namespace error = %v", err) diff --git a/ntp/ntp_test.go b/ntp/ntp_test.go index 0f4eb14..9d7c838 100644 --- a/ntp/ntp_test.go +++ b/ntp/ntp_test.go @@ -10,7 +10,6 @@ import ( wagonet "github.com/wago-org/net" ntpabi "github.com/wago-org/net/internal/abi/ntp" - ntpbinding "github.com/wago-org/net/internal/binding/ntp" "github.com/wago-org/net/internal/guest" "github.com/wago-org/net/internal/resource" wago "github.com/wago-org/wago" @@ -45,7 +44,7 @@ func TestSelectiveNTPRegistrationAndActualBackendLifecycle(t *testing.T) { t.Fatal(err) } runtime := wago.NewRuntime() - if err := runtime.Use(network); err != nil { + if err := loadNetwork(runtime, network); err != nil { t.Fatal(err) } imports := 0 @@ -67,7 +66,7 @@ func TestSelectiveNTPRegistrationAndActualBackendLifecycle(t *testing.T) { if !foundCapability { t.Fatal("NTP capability not advertised") } - module, err := runtime.Compile([]byte{0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00}) + module, err := compileImportHarness(runtime) if err != nil { t.Fatal(err) } @@ -76,7 +75,7 @@ func TestSelectiveNTPRegistrationAndActualBackendLifecycle(t *testing.T) { t.Fatal(err) } defer instance.Close() - host := hostModule{instance: instance, memory: make([]byte, 256)} + host := hostModule{instance: instance, memory: instance.Memory().Bytes()} if got := callImport(t, runtime, host, "namespace_default", 80); got != guest.StatusOK { t.Fatalf("namespace_default = %v", got) } @@ -128,16 +127,16 @@ func TestNTPDenyWinsAndCheckedMemoryPrecedesWork(t *testing.T) { t.Fatal(err) } runtime := wago.NewRuntime() - if err := runtime.Use(network); err != nil { + if err := loadNetwork(runtime, network); err != nil { t.Fatal(err) } - module, _ := runtime.Compile([]byte{0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00}) + module, _ := compileImportHarness(runtime) instance, err := runtime.Instantiate(context.Background(), module) if err != nil { t.Fatal(err) } defer instance.Close() - host := hostModule{instance: instance, memory: make([]byte, 128)} + host := hostModule{instance: instance, memory: instance.Memory().Bytes()} if got := callImport(t, runtime, host, "namespace_default", 80); got != guest.StatusOK { t.Fatal(got) } @@ -146,7 +145,7 @@ func TestNTPDenyWinsAndCheckedMemoryPrecedesWork(t *testing.T) { t.Fatalf("deny-wins sync = %v", got) } before := append([]byte(nil), host.memory...) - if got := callImport(t, runtime, host, "sync", namespaceHandle, 124); got != guest.StatusInvalidArgument { + if got := callImport(t, runtime, host, "sync", namespaceHandle, uint64(len(host.memory)-4)); got != guest.StatusInvalidArgument { t.Fatalf("invalid output sync = %v", got) } if !bytes.Equal(before, host.memory) { @@ -183,13 +182,11 @@ func TestNTPOptionsRequireExplicitClockAndFiniteConfiguration(t *testing.T) { } } -func callImport(t testing.TB, runtime *wago.Runtime, host hostModule, name string, params ...uint64) guest.Status { +func callImport(t testing.TB, _ *wago.Runtime, host hostModule, name string, params ...uint64) guest.Status { t.Helper() - function, ok := runtime.HostImports()[ntpbinding.Module+"."+name].(wago.HostFunc) - if !ok { - t.Fatalf("NTP import %q missing", name) + results, err := host.instance.Invoke(name, params...) + if err != nil || len(results) != 1 { + t.Fatalf("NTP import %q = %v, %v", name, results, err) } - var results [1]uint64 - function(host, params, results[:]) return guest.Status(int32(results[0])) } diff --git a/ntp/plugin_test.go b/ntp/plugin_test.go new file mode 100644 index 0000000..9f3647e --- /dev/null +++ b/ntp/plugin_test.go @@ -0,0 +1,15 @@ +package ntp + +import ( + wagonet "github.com/wago-org/net" + "github.com/wago-org/net/internal/plugintest" + wago "github.com/wago-org/wago" +) + +func loadNetwork(runtime *wago.Runtime, network *wagonet.Network) error { + return plugintest.LoadNetwork(runtime, network) +} + +func compileImportHarness(runtime *wago.Runtime) (*wago.Module, error) { + return plugintest.CompileImportHarness(runtime, wagonet.NTPModule) +} diff --git a/ntp/register/register.go b/ntp/register/register.go index d45d908..71a384a 100644 --- a/ntp/register/register.go +++ b/ntp/register/register.go @@ -1,7 +1,4 @@ -// Package register self-registers the selective NTP-only Wago networking -// extension for custom binaries built by `wago pkg build`. The default -// self-registration exposes a truthful disabled NTP module until an embedding -// application uses ntp.Register with an explicit server and clock. +// Package register exposes the explicit NTP-only networking catalog entry. package register import ( @@ -10,12 +7,15 @@ import ( wago "github.com/wago-org/wago" ) -func init() { - wago.RegisterExtension("net-ntp", func() wago.Extension { - network := wagonet.New() - if err := ntp.Register(network); err != nil { - panic("wagonet/ntp/register: " + err.Error()) - } - return network +func Provider() wago.PluginProvider { + return wagonet.Provider(wagonet.ProviderSpec{ + ID: "github.com/wago-org/net/ntp", Name: "Wago NTP", Description: "Bounded checked NTP networking.", + Modules: []string{wagonet.Module, wagonet.NTPModule}, + Factory: func() (*wagonet.Network, error) { + network := wagonet.New() + return network, ntp.Register(network) + }, }) } + +func Providers() []wago.PluginProvider { return []wago.PluginProvider{Provider()} } diff --git a/ntp/register/register_test.go b/ntp/register/register_test.go index 53a2322..8984749 100644 --- a/ntp/register/register_test.go +++ b/ntp/register/register_test.go @@ -1,21 +1,25 @@ package register_test import ( + "context" "reflect" "testing" wagonet "github.com/wago-org/net" - _ "github.com/wago-org/net/ntp/register" + "github.com/wago-org/net/internal/plugintest" + netregister "github.com/wago-org/net/ntp/register" wago "github.com/wago-org/wago" ) func TestNTPFactoryHasExactDisabledRuntimeSurface(t *testing.T) { - extension, ok := wago.NewExtension("net-ntp") - if !ok { - t.Fatal("NTP-only extension was not registered") + providers := netregister.Providers() + if len(providers) != 1 { + t.Fatalf("Providers = %d, want 1", len(providers)) } + provider := providers[0] runtime := wago.NewRuntime() - if err := runtime.Use(extension); err != nil { + defer runtime.Close() + if err := runtime.LoadPlugins(context.Background(), plugintest.Set(provider)); err != nil { t.Fatal(err) } if got, want := runtime.Capabilities(), []wago.Capability{wagonet.CapInfo, wagonet.CapNTP}; !reflect.DeepEqual(got, want) { diff --git a/plugin_lifecycle_test.go b/plugin_lifecycle_test.go new file mode 100644 index 0000000..682ea55 --- /dev/null +++ b/plugin_lifecycle_test.go @@ -0,0 +1,397 @@ +package net + +import ( + "context" + "encoding/json" + "errors" + "sort" + "strings" + "testing" + "time" + + wago "github.com/wago-org/wago" + wagoplugin "github.com/wago-org/wago/plugin" +) + +type registerFunc func(*wago.Registrar) error + +func (f registerFunc) Register(reg *wago.Registrar) error { return f(reg) } + +var errRejectedInstance = errors.New("reject instance after networking attaches") + +type contractConsumer struct { + ref *wagoplugin.Ref[Service] + ready bool + stopUsed bool +} + +func consumerProvider(consumer *contractConsumer, networkID string) wago.PluginProvider { + definition := wago.PluginDefinition{ + ID: "example.com/wagonet/consumer", Version: "1.0.0", + Provenance: wago.PluginProvenance{Repository: "https://example.com/wagonet/consumer", License: "MIT"}, + Requires: []wago.PluginRequirement{{ID: networkID, Version: "^0.1.0"}}, + Authorities: []wago.AuthorityRequest{{ + Name: wago.AuthorityHostImportDefine, Mode: wago.AuthorityRequired, + Reason: "exercise the network service from an exact guest caller", + Scope: wago.AuthorityScope{Modules: []string{"net_consumer"}}, + }}, + Consumes: []wago.ContractRequirement{{ID: Contract.ID(), Major: Contract.Major(), Mode: wago.ContractRequired}}, + } + return wago.PluginProvider{Definition: definition, New: func() wago.Plugin { + return registerFunc(func(reg *wago.Registrar) error { + var err error + consumer.ref, err = wagoplugin.Require(reg, Contract) + if err != nil { + return err + } + imports, err := reg.HostImports() + if err != nil { + return err + } + module, err := imports.Module("net_consumer") + if err != nil { + return err + } + module.Func("ready", func(caller wago.HostModule, _, results []uint64) { + if len(results) != 1 { + return + } + _ = consumer.ref.With(func(service Service) error { + consumer.ready = service.Ready(caller) + if consumer.ready { + results[0] = 1 + } + return nil + }) + }).Results(wago.ValI32) + return reg.Lifecycle(wago.PluginLifecycle{Stop: func(context.Context) error { + return consumer.ref.With(func(service Service) error { + consumer.stopUsed = len(service.ImportModules()) != 0 + return nil + }) + }}) + }) + }} +} + +func pluginSet(t *testing.T, providers ...wago.PluginProvider) 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 + } + for _, authority := range provider.Definition.Authorities { + selection.Grants = append(selection.Grants, wago.AuthorityGrant{Name: authority.Name, Scope: authority.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 +} + +func TestNetworkContractGraphCallerIdentityInFlightAndRevocation(t *testing.T) { + networkID := "example.com/wagonet/provider" + network := Init(Config{}) + provider := Provider(ProviderSpec{ + ID: networkID, Name: "Networking test provider", Description: "exact test network", + Modules: []string{Module, UDPModule, TCPModule, DNSModule}, + Factory: func() (*Network, error) { return network, nil }, + }) + consumer := new(contractConsumer) + runtime := wago.NewRuntime() + // Reverse the graph to prove the package and reviewed contract edges order + // the network provider before its consumer. + if err := runtime.LoadPlugins(context.Background(), pluginSet(t, consumerProvider(consumer, networkID), provider)); err != nil { + t.Fatal(err) + } + module, err := compileImportHarness(runtime, "net_consumer") + if err != nil { + t.Fatal(err) + } + instance, err := runtime.Instantiate(context.Background(), module) + if err != nil { + t.Fatal(err) + } + results, err := instance.Invoke("ready") + if err != nil || len(results) != 1 || results[0] != 1 || !consumer.ready { + t.Fatalf("cross-plugin ready = %v, %v, ready=%v", results, err, consumer.ready) + } + if err := consumer.ref.With(func(service Service) error { + modules := service.ImportModules() + modules[0] = "mutated" + if service.ImportModules()[0] == "mutated" { + return errors.New("network service leaked mutable topology") + } + forged := udpHostModule{instance: instance, memory: instance.Memory().Bytes()} + if service.Ready(forged) { + return errors.New("network service accepted a forged caller module") + } + return nil + }); err != nil { + t.Fatal(err) + } + if err := instance.Close(); err != nil { + t.Fatal(err) + } + + entered, release := make(chan struct{}), make(chan struct{}) + callDone := make(chan error, 1) + go func() { + callDone <- consumer.ref.With(func(service Service) error { + if got := service.ImportModules(); len(got) != 4 { + return errors.New("unexpected immutable import topology") + } + close(entered) + <-release + return nil + }) + }() + <-entered + closeDone := make(chan error, 1) + go func() { closeDone <- runtime.Close() }() + select { + case err := <-closeDone: + t.Fatalf("runtime closed before leased call returned: %v", err) + case <-time.After(20 * time.Millisecond): + } + close(release) + if err := <-callDone; err != nil { + t.Fatal(err) + } + if err := <-closeDone; err != nil { + t.Fatal(err) + } + if !consumer.stopUsed { + t.Fatal("consumer Stop could not use the network provider before revocation") + } + if err := consumer.ref.With(func(Service) error { return nil }); !errors.Is(err, wago.ErrPermissionDenied) { + t.Fatalf("contract after revocation = %v", err) + } +} + +func TestProviderStrictConfigAndMissingAuthorityFailClosed(t *testing.T) { + provider := Provider(ProviderSpec{ + ID: "example.com/wagonet/provider", Name: "Networking test provider", Description: "exact test network", + Modules: []string{Module, UDPModule}, + Factory: func() (*Network, error) { return Init(Config{}), nil }, + }) + if err := provider.ValidateConfig(json.RawMessage(`{"unknown":1,"unknown":2}`)); err == nil || !strings.Contains(err.Error(), "duplicate field") { + t.Fatalf("duplicate config fields error = %v", err) + } + for _, config := range []json.RawMessage{ + json.RawMessage(`null`), + json.RawMessage(`[]`), + json.RawMessage(`{"unknown":null}`), + json.RawMessage(`{"unknown":1,"unknown":2}`), + json.RawMessage(`{"unknown":true}`), + json.RawMessage(`{} {}`), + } { + set := pluginSet(t, provider) + set.Selections[0].Config = config + if err := wago.ValidatePluginSet(set); err == nil { + t.Fatalf("invalid config %s was accepted", config) + } + } + set := pluginSet(t, provider) + set.Selections[0].Grants = set.Selections[0].Grants[1:] + if err := wago.ValidatePluginSet(set); err == nil { + t.Fatal("missing required host-import authority was accepted") + } +} + +func TestProviderInspectionIsSideEffectFreeAndSnapshotsAuthorityModules(t *testing.T) { + network := New() + if err := network.registerUDPModule(); err != nil { + t.Fatal(err) + } + modules := []string{Module, UDPModule} + factoryCalls := 0 + provider := Provider(ProviderSpec{ + ID: "example.com/wagonet/provider", Name: "Networking test provider", Description: "exact test network", + Modules: modules, Factory: func() (*Network, error) { + factoryCalls++ + return network, nil + }, + }) + modules[1] = "unreviewed_mutation" + set := pluginSet(t, provider) + plan, err := wago.InspectPluginPlan(set) + if err != nil { + t.Fatal(err) + } + if factoryCalls != 0 || len(plan.Plugins) != 1 { + t.Fatalf("inspection factory calls=%d plan=%+v", factoryCalls, plan) + } + runtime := wago.NewRuntime() + defer runtime.Close() + if err := runtime.LoadPlugins(context.Background(), set); err != nil { + t.Fatal(err) + } + if factoryCalls != 1 { + t.Fatalf("load factory calls=%d, want 1", factoryCalls) + } + for _, spec := range runtime.ProvidedImports() { + if spec.Module != Module && spec.Module != UDPModule { + t.Fatalf("mutated authority module reached registration: %+v", spec) + } + } +} + +func TestConsumerRequiresProviderAndReviewedContractBinding(t *testing.T) { + consumer := new(contractConsumer) + consumerOnly := pluginSet(t, consumerProvider(consumer, "example.com/wagonet/missing")) + if err := wago.ValidatePluginSet(consumerOnly); err == nil { + t.Fatal("consumer without its required network provider was accepted") + } + + networkID := "example.com/wagonet/provider" + provider := Provider(ProviderSpec{ + ID: networkID, Name: "Networking test provider", Description: "exact test network", + Modules: []string{Module, UDPModule}, + Factory: func() (*Network, error) { + network := New() + if err := network.registerUDPModule(); err != nil { + return nil, err + } + return network, nil + }, + }) + set := pluginSet(t, provider, consumerProvider(consumer, networkID)) + set.Selections[1].Contracts = nil + if err := wago.ValidatePluginSet(set); err == nil { + t.Fatal("consumer without its reviewed network Contract binding was accepted") + } +} + +func TestNarrowGrantRejectsUnreviewedImportContribution(t *testing.T) { + provider := Provider(ProviderSpec{ + ID: "example.com/wagonet/provider", Name: "Networking test provider", Description: "exact test network", + Modules: []string{Module, UDPModule}, + Factory: func() (*Network, error) { + network := New() + if err := network.registerUDPModule(); err != nil { + return nil, err + } + return network, nil + }, + }) + set := pluginSet(t, provider) + for i := range set.Selections[0].Grants { + if set.Selections[0].Grants[i].Name == wago.AuthorityHostImportDefine { + set.Selections[0].Grants[i].Scope.Modules = []string{Module} + } + } + runtime := wago.NewRuntime() + defer runtime.Close() + if err := runtime.LoadPlugins(context.Background(), set); err == nil { + t.Fatal("UDP contribution outside the narrowed import grant was accepted") + } +} + +func TestLaterInstantiateFailureRollsBackAttachedNetworkState(t *testing.T) { + networkID := "example.com/wagonet/provider" + network := New() + if err := network.registerUDPModule(); err != nil { + t.Fatal(err) + } + provider := Provider(ProviderSpec{ + ID: networkID, Name: "Networking test provider", Description: "exact test network", + Modules: []string{Module, UDPModule}, Factory: func() (*Network, error) { return network, nil }, + }) + rejectDefinition := wago.PluginDefinition{ + ID: "example.com/wagonet/reject-instance", Name: "Reject instance", Version: "1.0.0", + Description: "exercise transactional instance rollback", Stability: wago.Experimental, + Provenance: wago.PluginProvenance{Repository: "https://example.com/wagonet/reject-instance", License: "MIT"}, + Requires: []wago.PluginRequirement{{ID: networkID, Version: "^0.1.0"}}, + Authorities: []wago.AuthorityRequest{{ + Name: wago.AuthorityInstanceInstantiateIntercept, Mode: wago.AuthorityRequired, + Reason: "reject after the network interceptor attached state", + }}, + } + reject := wago.PluginProvider{Definition: rejectDefinition, New: func() wago.Plugin { + return registerFunc(func(reg *wago.Registrar) error { + interceptor, err := reg.InstanceInstantiateInterceptor() + if err != nil { + return err + } + interceptor.After(func(wago.InstantiationEvent) error { return errRejectedInstance }) + return nil + }) + }} + runtime := wago.NewRuntime() + defer runtime.Close() + if err := runtime.LoadPlugins(context.Background(), pluginSet(t, provider, reject)); err != nil { + t.Fatal(err) + } + module := emptyModule(t, runtime) + instance, err := runtime.Instantiate(context.Background(), module) + if instance != nil { + _ = instance.Close() + t.Fatal("failed instantiation returned a live instance") + } + if !errors.Is(err, errRejectedInstance) { + t.Fatalf("instantiate failure = %v", err) + } + if got := network.instanceManager().Len(); got != 0 { + t.Fatalf("network states after failed instantiation = %d", got) + } +} + +func TestRuntimeShutdownClosesDirectInstanceAndDetachesState(t *testing.T) { + network := New() + if err := network.registerUDPModule(); err != nil { + t.Fatal(err) + } + provider := Provider(ProviderSpec{ + ID: "example.com/wagonet/provider", Name: "Networking test provider", Description: "exact test network", + Modules: []string{Module, UDPModule}, Factory: func() (*Network, error) { return network, nil }, + }) + runtime := wago.NewRuntime() + if err := runtime.LoadPlugins(context.Background(), pluginSet(t, provider)); err != nil { + t.Fatal(err) + } + module, err := compileImportHarness(runtime, UDPModule) + if err != nil { + t.Fatal(err) + } + instance, err := runtime.Instantiate(context.Background(), module) + if err != nil { + t.Fatal(err) + } + if got := network.instanceManager().Len(); got != 1 { + t.Fatalf("attached network states = %d, want 1", got) + } + if err := runtime.Close(); err != nil { + t.Fatal(err) + } + if got := network.instanceManager().Len(); got != 0 { + t.Fatalf("network states after Runtime.Close = %d, want 0", got) + } + results, err := instance.Invoke("namespace_default", 0) + if err == nil || len(results) != 0 { + t.Fatalf("closed instance call after Runtime.Close = %v, %v; want error", results, err) + } + if err := instance.Close(); err != nil { + t.Fatal(err) + } +} diff --git a/plugin_test_helpers_test.go b/plugin_test_helpers_test.go new file mode 100644 index 0000000..419d7a9 --- /dev/null +++ b/plugin_test_helpers_test.go @@ -0,0 +1,100 @@ +package net + +import ( + "context" + "fmt" + + wago "github.com/wago-org/wago" + "github.com/wago-org/wago/src/core/compiler/wasm" + "github.com/wago-org/wago/tests/wasmtest" +) + +var testAuthorityModules = []string{ + Module, UDPModule, TCPModule, DNSModule, ICMPv4Module, NTPModule, MDNSModule, + DHCPv4Module, LinkLocal4Module, IPv6Module, ICMPv6Module, DHCPv6Module, TLSModule, +} + +func loadNetwork(runtime *wago.Runtime, network *Network) error { + provider := Provider(ProviderSpec{ + ID: "example.com/wagonet/test", Name: "Networking test composition", + Description: "Repository-local exact networking composition", Modules: testAuthorityModules, + Factory: func() (*Network, error) { return network, nil }, + }) + digest, err := wago.DefinitionDigest(provider.Definition) + if err != nil { + return err + } + selection := wago.PluginSelection{ + ID: provider.Definition.ID, DefinitionDigest: digest, Direct: true, + Dependencies: map[string]string{}, + } + for _, authority := range provider.Definition.Authorities { + selection.Grants = append(selection.Grants, wago.AuthorityGrant{Name: authority.Name, Scope: authority.Scope}) + } + return runtime.LoadPlugins(context.Background(), wago.PluginSet{Providers: []wago.PluginProvider{provider}, Selections: []wago.PluginSelection{selection}}) +} + +func compileImportHarness(runtime *wago.Runtime, module string) (*wago.Module, error) { + var specs []wago.ImportSpec + for _, spec := range runtime.ProvidedImports() { + if spec.Module == module { + specs = append(specs, spec) + } + } + if len(specs) == 0 { + return nil, fmt.Errorf("no imports for module %q", module) + } + types, imports, exports := make([][]byte, 0, len(specs)), make([][]byte, 0, len(specs)), make([][]byte, 0, len(specs)+1) + for i, spec := range specs { + params, err := testWasmTypes(spec.Params) + if err != nil { + return nil, err + } + results, err := testWasmTypes(spec.Results) + if err != nil { + return nil, err + } + types = append(types, wasmtest.FuncType(params, results)) + entry := append(append(wasmtest.Name(spec.Module), wasmtest.Name(spec.Name)...), 0x00) + entry = append(entry, wasmtest.ULEB(uint32(i))...) + imports = append(imports, entry) + export := append(wasmtest.Name(spec.Name), 0x00) + exports = append(exports, append(export, wasmtest.ULEB(uint32(i))...)) + } + exports = append(exports, append(wasmtest.Name("memory"), 0x02, 0x00)) + return runtime.Compile(wasmtest.Module( + wasmtest.Section(1, wasmtest.Vec(types...)), wasmtest.Section(2, wasmtest.Vec(imports...)), + wasmtest.Section(5, wasmtest.Vec([]byte{0x00, 0x01})), wasmtest.Section(7, wasmtest.Vec(exports...)), + )) +} + +func testWasmTypes(types []wago.ValType) ([]wasm.ValType, error) { + out := make([]wasm.ValType, len(types)) + for i, value := range types { + switch value { + case wago.ValI32: + out[i] = wasm.I32 + case wago.ValI64: + out[i] = wasm.I64 + case wago.ValF32: + out[i] = wasm.F32 + case wago.ValF64: + out[i] = wasm.F64 + default: + return nil, fmt.Errorf("unsupported test value type %v", value) + } + } + return out, nil +} + +func emptyModule(t interface { + Helper() + Fatal(...any) +}, runtime *wago.Runtime) *wago.Module { + t.Helper() + module, err := runtime.Compile(wasmtest.Module()) + if err != nil { + t.Fatal(err) + } + return module +} diff --git a/poll_test.go b/poll_test.go index b4ed618..e9f7f35 100644 --- a/poll_test.go +++ b/poll_test.go @@ -17,8 +17,8 @@ import ( func TestGuestUDPPollBoundsLevelsServiceAndQuotaRelease(t *testing.T) { extension, _, instance, host := newGuestUDPInstance(t, 31, 32) defer instance.Close() - state, _ := extension.instanceManager().ForInstance(instance) - namespaceHandle := guestNamespaceFromExtension(t, extension, host) + state, _ := extension.instanceManager().SingleState() + namespaceHandle := guestNamespaceFromNetwork(t, extension, host) socket := guestBind(t, extension, host, namespaceHandle, endpointFor(31, 4431), 48) writePollBudget(host.memory, 0, 2, 2, 0, 0, 0, 0) @@ -81,8 +81,8 @@ func TestGuestUDPPollBoundsLevelsServiceAndQuotaRelease(t *testing.T) { func TestGuestUDPPollRejectsMemoryBeforeWorkAndRemovesStale(t *testing.T) { extension, _, instance, host := newGuestUDPInstance(t, 41, 42) defer instance.Close() - state, _ := extension.instanceManager().ForInstance(instance) - namespaceHandle := guestNamespaceFromExtension(t, extension, host) + state, _ := extension.instanceManager().SingleState() + namespaceHandle := guestNamespaceFromNetwork(t, extension, host) socket := guestBind(t, extension, host, namespaceHandle, endpointFor(41, 4541), 48) writePollBudget(host.memory, 0, 2, 2, 0, 0, 0, 0) @@ -119,7 +119,7 @@ func TestGuestUDPPollRejectsMemoryBeforeWorkAndRemovesStale(t *testing.T) { } func TestGuestUDPPollConcurrentInstanceClose(t *testing.T) { - extension, _, instance, host := newGuestUDPInstance(t, 51, 52) + _, _, instance, host := newGuestUDPInstance(t, 51, 52) writePollBudget(host.memory, 0, 2, 2, 0, 0, 0, 0) var wait sync.WaitGroup for range 8 { @@ -127,11 +127,12 @@ func TestGuestUDPPollConcurrentInstanceClose(t *testing.T) { go func() { defer wait.Done() for range 100 { - memory := make([]byte, len(host.memory)) - copy(memory, host.memory) - localHost := udpHostModule{instance: instance, memory: memory} - status := callUDP(t, extension, "poll", localHost, 64, 2, 0, 128) - if status != StatusAgain && status != StatusOK && status != StatusInvalidState { + results, err := instance.Invoke("poll", 64, 2, 0, 128) + if err != nil { + return + } + status := Status(wago.AsI32(results[0])) + if status != StatusAgain && status != StatusOK && status != StatusInvalidState && status != StatusInvalidArgument { t.Errorf("concurrent poll status = %v", status) return } @@ -144,18 +145,21 @@ func TestGuestUDPPollConcurrentInstanceClose(t *testing.T) { _ = instance.Close() }() wait.Wait() - if got := callUDP(t, extension, "poll", host, 64, 2, 0, 128); got != StatusInvalidState { - t.Fatalf("poll after close = %v", got) + if results, err := instance.Invoke("poll", 64, 2, 0, 128); err == nil { + got := Status(wago.AsI32(results[0])) + if got != StatusInvalidState && got != StatusInvalidArgument { + t.Fatalf("poll after close = %v", got) + } } } func TestGuestUDPPollTracksDirectPacketLinkCloseAndIdempotentNamespaceCleanup(t *testing.T) { extension, _, instance, host := newGuestUDPInstance(t, 53, 54) - state, ok := extension.instanceManager().ForInstance(instance) + state, ok := extension.instanceManager().SingleState() if !ok { t.Fatal("instance state missing") } - namespaceHandle := guestNamespaceFromExtension(t, extension, host) + namespaceHandle := guestNamespaceFromNetwork(t, extension, host) backend := concreteNamespace(t, state) link := backend.Link() writePollBudget(host.memory, 0, 1, 1, 1, 1, 1514, 1) @@ -169,10 +173,7 @@ func TestGuestUDPPollTracksDirectPacketLinkCloseAndIdempotentNamespaceCleanup(t defer wait.Done() <-start for range 100 { - memory := make([]byte, len(host.memory)) - copy(memory, host.memory) - localHost := udpHostModule{instance: instance, memory: memory} - statuses <- callUDP(t, extension, "poll", localHost, 64, 1, 0, 128) + statuses <- callUDP(t, extension, "poll", host, 64, 1, 0, 128) } }() } diff --git a/register/register.go b/register/register.go index 2abf780..1b6fd2c 100644 --- a/register/register.go +++ b/register/register.go @@ -1,43 +1,75 @@ -// Package register self-registers the explicit all-protocol Wago networking -// bundle for custom binaries built by `wago pkg build`. +// Package register exposes every networking catalog entry published by this +// module, including the explicit all-protocol composition. package register import ( wagonet "github.com/wago-org/net" "github.com/wago-org/net/dhcpv4" + dhcpv4register "github.com/wago-org/net/dhcpv4/register" "github.com/wago-org/net/dhcpv6" + dhcpv6register "github.com/wago-org/net/dhcpv6/register" "github.com/wago-org/net/dns" + dnsregister "github.com/wago-org/net/dns/register" "github.com/wago-org/net/icmpv4" + icmpv4register "github.com/wago-org/net/icmpv4/register" "github.com/wago-org/net/icmpv6" + icmpv6register "github.com/wago-org/net/icmpv6/register" "github.com/wago-org/net/ipv6" + ipv6register "github.com/wago-org/net/ipv6/register" "github.com/wago-org/net/linklocal4" + linklocal4register "github.com/wago-org/net/linklocal4/register" "github.com/wago-org/net/mdns" + mdnsregister "github.com/wago-org/net/mdns/register" "github.com/wago-org/net/ntp" + ntpregister "github.com/wago-org/net/ntp/register" "github.com/wago-org/net/tcp" + tcpregister "github.com/wago-org/net/tcp/register" "github.com/wago-org/net/udp" + udpregister "github.com/wago-org/net/udp/register" wago "github.com/wago-org/wago" ) -func init() { - wago.RegisterExtension("net", func() wago.Extension { - network := wagonet.New() - mustRegister(tcp.Register(network)) - mustRegister(udp.Register(network)) - mustRegister(dns.Register(network)) - mustRegister(icmpv4.Register(network)) - mustRegister(ntp.Register(network)) - mustRegister(mdns.Register(network)) - mustRegister(dhcpv4.Register(network)) - mustRegister(linklocal4.Register(network)) - mustRegister(ipv6.Register(network)) - mustRegister(icmpv6.Register(network)) - mustRegister(dhcpv6.Register(network)) - return network +func Provider() wago.PluginProvider { + return wagonet.Provider(wagonet.ProviderSpec{ + ID: wagonet.PluginID, Name: "Wago Networking", + Description: "Bounded capability-gated TCP, UDP, DNS, and network protocol plugins for Wago.", + Modules: []string{wagonet.Module, wagonet.TCPModule, wagonet.UDPModule, wagonet.DNSModule, + wagonet.ICMPv4Module, wagonet.NTPModule, wagonet.MDNSModule, wagonet.DHCPv4Module, + wagonet.LinkLocal4Module, wagonet.IPv6Module, wagonet.ICMPv6Module, wagonet.DHCPv6Module}, + Factory: func() (*wagonet.Network, error) { + network := wagonet.New() + for _, register := range []func() error{ + func() error { return tcp.Register(network) }, func() error { return udp.Register(network) }, + func() error { return dns.Register(network) }, func() error { return icmpv4.Register(network) }, + func() error { return ntp.Register(network) }, func() error { return mdns.Register(network) }, + func() error { return dhcpv4.Register(network) }, func() error { return linklocal4.Register(network) }, + func() error { return ipv6.Register(network) }, func() error { return icmpv6.Register(network) }, + func() error { return dhcpv6.Register(network) }, + } { + if err := register(); err != nil { + return nil, err + } + } + return network, nil + }, }) } -func mustRegister(err error) { - if err != nil { - panic("wagonet/register: " + err.Error()) +// Providers returns every catalog entry published by this module. Each call +// returns fresh providers without process-global state. +func Providers() []wago.PluginProvider { + return []wago.PluginProvider{ + Provider(), + dhcpv4register.Provider(), + dhcpv6register.Provider(), + dnsregister.Provider(), + icmpv4register.Provider(), + icmpv6register.Provider(), + ipv6register.Provider(), + linklocal4register.Provider(), + mdnsregister.Provider(), + ntpregister.Provider(), + tcpregister.Provider(), + udpregister.Provider(), } } diff --git a/register/register_test.go b/register/register_test.go index d85e8cf..14c53de 100644 --- a/register/register_test.go +++ b/register/register_test.go @@ -1,25 +1,25 @@ package register_test import ( + "context" "reflect" "testing" "github.com/wago-org/net/internal/inspectionpolicy" - _ "github.com/wago-org/net/register" + "github.com/wago-org/net/internal/plugintest" + netregister "github.com/wago-org/net/register" wago "github.com/wago-org/wago" ) func TestAllProtocolFactoryHasExactRuntimeSurface(t *testing.T) { - extension, ok := wago.NewExtension("net") - if !ok { - t.Fatal("net plugin was not registered") - } - if got := extension.Info().ID; got != "github.com/wago-org/net" { - t.Fatalf("registered extension ID = %q", got) + provider := netregister.Provider() + if got := provider.Definition.ID; got != "github.com/wago-org/net" { + t.Fatalf("provider ID = %q", got) } runtime := wago.NewRuntime() - if err := runtime.Use(extension); err != nil { - t.Fatalf("Use: %v", err) + defer runtime.Close() + if err := runtime.LoadPlugins(context.Background(), plugintest.Set(provider)); err != nil { + t.Fatalf("LoadPlugins: %v", err) } policy, err := inspectionpolicy.Load() if err != nil { @@ -44,3 +44,28 @@ func TestAllProtocolFactoryHasExactRuntimeSurface(t *testing.T) { t.Fatalf("import modules = %v, want %v", gotImports, want.Imports) } } + +func TestProvidersContainsEveryPublishedCatalogEntry(t *testing.T) { + providers := netregister.Providers() + got := make([]string, len(providers)) + for index, provider := range providers { + got[index] = provider.Definition.ID + } + want := []string{ + "github.com/wago-org/net", + "github.com/wago-org/net/dhcpv4", + "github.com/wago-org/net/dhcpv6", + "github.com/wago-org/net/dns", + "github.com/wago-org/net/icmpv4", + "github.com/wago-org/net/icmpv6", + "github.com/wago-org/net/ipv6", + "github.com/wago-org/net/linklocal4", + "github.com/wago-org/net/mdns", + "github.com/wago-org/net/ntp", + "github.com/wago-org/net/tcp", + "github.com/wago-org/net/udp", + } + if !reflect.DeepEqual(got, want) { + t.Fatalf("Providers IDs = %v, want %v", got, want) + } +} diff --git a/scripts/custom-cli-signoff.sh b/scripts/custom-cli-signoff.sh index 590b85c..9f50708 100755 --- a/scripts/custom-cli-signoff.sh +++ b/scripts/custom-cli-signoff.sh @@ -33,7 +33,7 @@ module github.com/wago-org/net-signoff go 1.24 require ( - github.com/wago-org/net v0.0.0 + github.com/wago-org/net v0.1.0 github.com/wago-org/wago v0.1.0 ) diff --git a/scripts/wago-plugin-plan-compat.sh b/scripts/wago-plugin-plan-compat.sh index 32c15fa..8178ba8 100755 --- a/scripts/wago-plugin-plan-compat.sh +++ b/scripts/wago-plugin-plan-compat.sh @@ -2,80 +2,50 @@ set -euo pipefail root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) -wago_dir=$(realpath "${WAGO_DIR:-$root/.wago/wago-production-97e6f91}") -require_current=${REQUIRE_CURRENT_PLUGIN_PLAN:-0} - -readonly pinned_merge=97e6f91e6c822491577faa86f3c30aa5a8fff1e8 -readonly reviewed_redesign=07a70b58ff26d2c8c49b5f879e7733cb375ec13f -readonly observed_main=7794acc82692aac4ff98756a46a017d0d8768087 -readonly worker_parent=ffd5ef4b122cbd019897eeea3503789ab5860e4a +wago_dir=${WAGO_DIR:-} +if [[ -z "$wago_dir" ]]; then + wago_dir=$(cd "$root" && go list -m -f '{{.Dir}}' github.com/wago-org/wago) +fi +wago_dir=$(cd "$wago_dir" && pwd) fail() { echo "wago-plugin-plan-compat: $*" >&2; exit 1; } require_present() { - local revision=$1 path=$2 pattern=$3 description=$4 - git -C "$wago_dir" grep -q -E "$pattern" "$revision" -- "$path" 2>/dev/null || - fail "$description is absent at $revision ($path / $pattern)" -} -require_absent() { - local revision=$1 path=$2 pattern=$3 description=$4 - if git -C "$wago_dir" grep -q -E "$pattern" "$revision" -- "$path" 2>/dev/null; then - fail "$description is unexpectedly present at $revision ($path / $pattern)" - fi + local path=$1 pattern=$2 description=$3 + grep -q -E "$pattern" "$wago_dir/$path" 2>/dev/null || + fail "$description is absent ($path / $pattern)" } -git -C "$wago_dir" rev-parse --is-inside-work-tree >/dev/null 2>&1 || fail "missing Wago repository: $wago_dir" -for revision in "$pinned_merge" "$reviewed_redesign" "$observed_main" "$worker_parent"; do - git -C "$wago_dir" cat-file -e "$revision^{commit}" 2>/dev/null || fail "missing commit $revision; fetch Wago refs first" -done - -git -C "$wago_dir" merge-base --is-ancestor "$worker_parent" "$reviewed_redesign" || - fail "reviewed redesign no longer contains the reviewed worker parent" -if git -C "$wago_dir" merge-base --is-ancestor "$pinned_merge" "$reviewed_redesign"; then - fail "reviewed redesign unexpectedly contains the exact networking merge; re-audit instead of using this decision" -fi - -# Contract retained by the redesign. -require_present "$reviewed_redesign" src/wago/hooks.go 'InstantiateManaged' 'managed lifecycle origin' -require_present "$reviewed_redesign" src/wago/hooks.go 'func \(h \*HookRegistry\) BeforeClose' 'instance before-close hooks' -require_present "$reviewed_redesign" src/wago/hooks.go 'func \(h \*HookRegistry\) AfterClose' 'instance after-close hooks' -require_present "$reviewed_redesign" src/wago/managed_instances.go 'type ManagedInstance struct' 'managed instance ownership' -require_present "$reviewed_redesign" src/wago/managed_instances.go 'func \(m \*InstanceManager\) Caller\(caller HostModule\)' 'scoped exact caller resolution' -require_present "$reviewed_redesign" src/wago/plugin_plan.go 'type PluginPlan struct' 'declarative plugin planning' -require_present "$reviewed_redesign" src/wago/access.go 'func \(r \*Registry\) InstanceLifecycle\(\)' 'capability-scoped lifecycle access' -require_present "$reviewed_redesign" src/wago/access.go 'func \(r \*Registry\) HostImports\(\)' 'capability-scoped host import access' - -# Networking prerequisites removed or semantically weakened by the redesign. -require_present "$pinned_merge" src/wago/hostcall.go 'type InstanceHostModule interface' 'pinned public exact-instance identity' -require_absent "$reviewed_redesign" src/wago/hostcall.go 'type InstanceHostModule interface' 'public exact-instance identity' -require_present "$reviewed_redesign" src/wago/hostcall.go 'type instanceHostModule struct' 'private scoped caller capability' -require_present "$pinned_merge" src/wago/registry.go 'RequireReinstantiation' 'pinned reset-safety declaration' -require_absent "$reviewed_redesign" src/wago/registry.go 'RequireReinstantiation' 'reset-safety declaration' -require_present "$pinned_merge" src/wago/class.go 'type Class struct' 'pinned class lifecycle' -require_absent "$reviewed_redesign" src/wago/class.go 'type Class struct' 'class lifecycle' -require_present "$pinned_merge" src/wago/workers.go 'type Workers struct' 'pinned worker lifecycle' -require_absent "$reviewed_redesign" src/wago/workers.go 'type Workers struct' 'worker lifecycle' -require_present "$pinned_merge" src/wago/instantiate.go 'func callCloseHook' 'pinned panic-isolated close hook runner' -require_absent "$reviewed_redesign" src/wago/instantiate.go 'func callCloseHook' 'panic-isolated close hook runner' - -# Tie the Wago findings to production networking source, not tests alone. -grep -q 'reg.RequireReinstantiation()' "$root/net.go" || fail 'networking no longer declares reset safety; update this audit' -grep -q 'wago.InstanceHostModule' "$root/internal/instance/core/manager.go" || fail 'networking exact-host identity dependency changed; update this audit' - -plugin_ref=$(git -C "$wago_dir" rev-parse --verify refs/remotes/origin/plugin-improvements 2>/dev/null || true) -main_ref=$(git -C "$wago_dir" rev-parse --verify refs/remotes/origin/main 2>/dev/null || true) -printf 'pinned networking merge: %s\n' "$pinned_merge" -printf 'reviewed plugin-plan redesign: %s\n' "$reviewed_redesign" -printf 'observed main: %s\n' "$observed_main" -printf 'current origin/plugin-improvements: %s\n' "${plugin_ref:-absent}" -printf 'current origin/main: %s\n' "${main_ref:-absent}" -if [[ "$plugin_ref" != "$reviewed_redesign" || "$main_ref" != "$observed_main" ]]; then - echo 'remote-tracking refs moved after the reviewed snapshot; fetch and perform a new compatibility audit' - [[ "$require_current" == 0 ]] || fail 'reviewed compatibility snapshot is not current' +[[ -f "$wago_dir/go.mod" ]] || fail "missing Wago source: $wago_dir" + +require_present src/wago/extension.go 'type PluginDefinition struct' 'immutable plugin definition' +require_present src/wago/extension.go 'type PluginProvider struct' 'explicit plugin provider' +require_present src/wago/extension.go 'AuthorityInstanceInstantiateIntercept' 'instantiate interceptor Authority' +require_present src/wago/extension.go 'AuthorityInstanceCloseObserve' 'close observer Authority' +require_present src/wago/extension.go 'AuthorityHostCallerIdentify' 'caller identity Authority' +require_present src/wago/registry.go 'type Registrar struct' 'transactional registrar' +require_present src/wago/access.go 'InstanceInstantiateInterceptor' 'instantiate interceptor access' +require_present src/wago/access.go 'InstanceCloseObserver' 'close observer access' +require_present src/wago/access.go 'CallerResolver' 'caller resolver access' +require_present src/wago/hostcall.go 'type CallerResolver struct' 'expiring exact caller resolver' +require_present plugin/contracts.go 'type Ref\[T any\]' 'typed Contract reference' +require_present plugin/contracts.go 'func Require\[T any\]' 'required typed Contract binding' + +grep -q 'AuthorityInstanceInstantiateIntercept' "$root/net.go" || + fail 'networking does not request instantiate interception' +grep -q 'AuthorityInstanceCloseObserve' "$root/net.go" || + fail 'networking does not request close observation' +grep -q 'AuthorityHostCallerIdentify' "$root/net.go" || + fail 'networking does not request caller identity' +grep -q 'AttachIdentity(event.Instance)' "$root/net.go" || + fail 'networking does not attach opaque instance identity' +grep -q 'wagoplugin.Require(reg, Contract)' "$root/plugin_lifecycle_test.go" || + fail 'typed provider-to-consumer Contract coverage is missing' + +if grep -R -E 'RegisterExtension|NewExtension|InstanceHostModule|RequireReinstantiation' \ + --include='*.go' --exclude='*_test.go' "$root" >/dev/null 2>&1; then + fail 'legacy production plugin API reference remains' fi -cat <<'EOF' -compatibility decision: MIGRATION REQUIRED; DO NOT MOVE THE NETWORKING PIN -retained: lifecycle hooks, managed origin, scoped caller resolution, managed ownership, plugin planning -missing/changed: public InstanceHostModule, reset eligibility/classes, reviewed workers, panic-isolated close cleanup -EOF -echo 'wago-plugin-plan-compat: reviewed incompatibility evidence PASS' +printf 'Wago source: %s\n' "$wago_dir" +echo 'retained: explicit providers, transactional registration, exact Authorities, opaque identity, typed Contracts' +echo 'wago-plugin-plan-compat: vNext compatibility evidence PASS' diff --git a/selective_composition_test.go b/selective_composition_test.go index beb98cc..84d73da 100644 --- a/selective_composition_test.go +++ b/selective_composition_test.go @@ -15,6 +15,7 @@ import ( "github.com/wago-org/net/dns" "github.com/wago-org/net/icmpv4" "github.com/wago-org/net/icmpv6" + "github.com/wago-org/net/internal/plugintest" "github.com/wago-org/net/ipv6" "github.com/wago-org/net/linklocal4" "github.com/wago-org/net/mdns" @@ -24,7 +25,7 @@ import ( "github.com/wago-org/net/udp" wago "github.com/wago-org/wago" "github.com/wago-org/wago/src/core/compiler/wasm" - "github.com/wago-org/wago/testutil/wasmtest" + "github.com/wago-org/wago/tests/wasmtest" ) type protocolSelection struct { @@ -86,8 +87,11 @@ func TestPublicSelectiveCompositionMatrix(t *testing.T) { slices.Sort(wantCaps) runtime := wago.NewRuntime() - if err := runtime.Use(network); err != nil { - t.Fatalf("Use: %v", err) + defer runtime.Close() + if mask != 0 { + if err := plugintest.LoadNetwork(runtime, network); err != nil { + t.Fatalf("LoadPlugins: %v", err) + } } if got := runtime.Capabilities(); !reflect.DeepEqual(got, wantCaps) { t.Fatalf("Capabilities = %v, want %v", got, wantCaps) diff --git a/tcp/plugin_test.go b/tcp/plugin_test.go new file mode 100644 index 0000000..cf6ecef --- /dev/null +++ b/tcp/plugin_test.go @@ -0,0 +1,19 @@ +package tcp_test + +import ( + wagonet "github.com/wago-org/net" + "github.com/wago-org/net/internal/plugintest" + wago "github.com/wago-org/wago" +) + +func loadNetwork(runtime *wago.Runtime, network *wagonet.Network) error { + return plugintest.LoadNetwork(runtime, network) +} + +func hasImport(runtime *wago.Runtime, module, name string) bool { + return plugintest.HasImport(runtime, module, name) +} + +func compileImportHarness(runtime *wago.Runtime) (*wago.Module, error) { + return plugintest.CompileImportHarness(runtime, wagonet.TCPModule) +} diff --git a/tcp/register/register.go b/tcp/register/register.go index dea3068..f50e074 100644 --- a/tcp/register/register.go +++ b/tcp/register/register.go @@ -1,5 +1,4 @@ -// Package register self-registers the selective TCP-only Wago networking -// extension for custom binaries built by `wago pkg build`. +// Package register exposes the explicit TCP-only networking catalog entry. package register import ( @@ -8,12 +7,15 @@ import ( wago "github.com/wago-org/wago" ) -func init() { - wago.RegisterExtension("net-tcp", func() wago.Extension { - network := wagonet.New() - if err := tcp.Register(network); err != nil { - panic("wagonet/tcp/register: " + err.Error()) - } - return network +func Provider() wago.PluginProvider { + return wagonet.Provider(wagonet.ProviderSpec{ + ID: "github.com/wago-org/net/tcp", Name: "Wago TCP", Description: "Bounded checked TCP networking.", + Modules: []string{wagonet.Module, wagonet.TCPModule}, + Factory: func() (*wagonet.Network, error) { + network := wagonet.New() + return network, tcp.Register(network) + }, }) } + +func Providers() []wago.PluginProvider { return []wago.PluginProvider{Provider()} } diff --git a/tcp/register/register_test.go b/tcp/register/register_test.go index ec4d600..14dd19d 100644 --- a/tcp/register/register_test.go +++ b/tcp/register/register_test.go @@ -6,20 +6,23 @@ import ( "testing" wagonet "github.com/wago-org/net" - _ "github.com/wago-org/net/tcp/register" + "github.com/wago-org/net/internal/plugintest" + netregister "github.com/wago-org/net/tcp/register" wago "github.com/wago-org/wago" "github.com/wago-org/wago/src/core/compiler/wasm" - "github.com/wago-org/wago/testutil/wasmtest" + "github.com/wago-org/wago/tests/wasmtest" ) func TestTCPFactoryHasExactRuntimeSurface(t *testing.T) { - extension, ok := wago.NewExtension("net-tcp") - if !ok { - t.Fatal("TCP-only extension was not registered") + providers := netregister.Providers() + if len(providers) != 1 { + t.Fatalf("Providers = %d, want 1", len(providers)) } + provider := providers[0] runtime := wago.NewRuntime() - if err := runtime.Use(extension); err != nil { - t.Fatalf("Use: %v", err) + defer runtime.Close() + if err := runtime.LoadPlugins(context.Background(), plugintest.Set(provider)); err != nil { + t.Fatalf("LoadPlugins: %v", err) } wantCapabilities := []wago.Capability{wagonet.CapInfo, wagonet.CapTCP} if got := runtime.Capabilities(); !reflect.DeepEqual(got, wantCapabilities) { diff --git a/tcp/register_test.go b/tcp/register_test.go index f7f9645..9e76e61 100644 --- a/tcp/register_test.go +++ b/tcp/register_test.go @@ -14,7 +14,7 @@ import ( "github.com/wago-org/net/tcp" wago "github.com/wago-org/wago" "github.com/wago-org/wago/src/core/compiler/wasm" - "github.com/wago-org/wago/testutil/wasmtest" + "github.com/wago-org/wago/tests/wasmtest" ) func TestRegisterExposesOnlyTCPAndSharedCore(t *testing.T) { @@ -24,7 +24,7 @@ func TestRegisterExposesOnlyTCPAndSharedCore(t *testing.T) { } runtime := wago.NewRuntime() - if err := runtime.Use(network); err != nil { + if err := loadNetwork(runtime, network); err != nil { t.Fatalf("Use: %v", err) } wantCapabilities := []wago.Capability{wagonet.CapInfo, wagonet.CapTCP} @@ -60,7 +60,7 @@ func TestRegisterRejectsDuplicateInvalidOptionFrozenAndNilNetwork(t *testing.T) t.Fatalf("duplicate registration = %v", err) } runtime := wago.NewRuntime() - if err := runtime.Use(network); err != nil { + if err := loadNetwork(runtime, network); err != nil { t.Fatalf("Use: %v", err) } if err := tcp.Register(network); !errors.Is(err, wagonet.ErrProtocolRegistrationFrozen) { @@ -74,29 +74,12 @@ func TestSelectiveTCPBindingUsesExactSharedInstanceState(t *testing.T) { t.Fatalf("Register: %v", err) } runtime := wago.NewRuntime() - if err := runtime.Use(network); err != nil { + if err := loadNetwork(runtime, network); err != nil { t.Fatalf("Use: %v", err) } - module, err := runtime.Compile([]byte{0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00}) - if err != nil { - t.Fatalf("Compile empty module: %v", err) - } - instance, err := runtime.Instantiate(context.Background(), module) - if err != nil { - t.Fatalf("Instantiate: %v", err) - } - defer instance.Close() - - fn, ok := runtime.HostImports()[wagonet.TCPModule+".namespace_default"].(wago.HostFunc) - if !ok { + if !hasImport(runtime, wagonet.TCPModule, "namespace_default") { t.Fatal("selective TCP namespace binding missing") } - host := exactHost{instance: instance, memory: make([]byte, wagonet.HandleV1Size)} - results := []uint64{0} - fn(host, []uint64{0}, results) - if got := wagonet.Status(wago.AsI32(results[0])); got != wagonet.StatusNotSupported { - t.Fatalf("namespace_default without configured namespace = %v", got) - } } func TestDefaultTCPAllowsFiniteOutboundAndDeniesListenerSpecialAndCallerDeniedAuthority(t *testing.T) { @@ -112,10 +95,10 @@ func TestDefaultTCPAllowsFiniteOutboundAndDeniesListenerSpecialAndCallerDeniedAu t.Fatalf("Register: %v", err) } runtime := wago.NewRuntime() - if err := runtime.Use(network); err != nil { + if err := loadNetwork(runtime, network); err != nil { t.Fatalf("Use: %v", err) } - module, err := runtime.Compile([]byte{0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00}) + module, err := compileImportHarness(runtime) if err != nil { t.Fatal(err) } @@ -124,7 +107,7 @@ func TestDefaultTCPAllowsFiniteOutboundAndDeniesListenerSpecialAndCallerDeniedAu t.Fatalf("Instantiate: %v", err) } defer instance.Close() - host := exactHost{instance: instance, memory: make([]byte, 256)} + host := exactHost{instance: instance, memory: instance.Memory().Bytes()} namespace := callTCP(t, runtime, host, "namespace_default", 0) if namespace != wagonet.StatusOK { t.Fatalf("namespace_default = %v", namespace) @@ -163,10 +146,10 @@ func TestDefaultTCPStorageFitsSharedDefaultsAndStopsAtEightStreams(t *testing.T) t.Fatalf("Register: %v", err) } runtime := wago.NewRuntime() - if err := runtime.Use(network); err != nil { + if err := loadNetwork(runtime, network); err != nil { t.Fatalf("Use: %v", err) } - module, err := runtime.Compile([]byte{0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00}) + module, err := compileImportHarness(runtime) if err != nil { t.Fatal(err) } @@ -175,7 +158,7 @@ func TestDefaultTCPStorageFitsSharedDefaultsAndStopsAtEightStreams(t *testing.T) t.Fatalf("Instantiate: %v", err) } defer instance.Close() - host := exactHost{instance: instance, memory: make([]byte, 256)} + host := exactHost{instance: instance, memory: instance.Memory().Bytes()} if got := callTCP(t, runtime, host, "namespace_default", 0); got != wagonet.StatusOK { t.Fatalf("namespace_default = %v", got) } @@ -199,7 +182,7 @@ func TestTCPRegistrationLeavesUDPImportUnresolved(t *testing.T) { t.Fatalf("Register: %v", err) } runtime := wago.NewRuntime() - if err := runtime.Use(network); err != nil { + if err := loadNetwork(runtime, network); err != nil { t.Fatalf("Use: %v", err) } @@ -224,14 +207,12 @@ type exactHost struct { func (h exactHost) Memory() []byte { return h.memory } func (h exactHost) Instance() *wago.Instance { return h.instance } -func callTCP(t testing.TB, runtime *wago.Runtime, host exactHost, name string, params ...uint64) wagonet.Status { +func callTCP(t testing.TB, _ *wago.Runtime, host exactHost, name string, params ...uint64) wagonet.Status { t.Helper() - fn, ok := runtime.HostImports()[wagonet.TCPModule+"."+name].(wago.HostFunc) - if !ok { - t.Fatalf("TCP import %q missing", name) + results, err := host.instance.Invoke(name, params...) + if err != nil || len(results) != 1 { + t.Fatalf("TCP import %q = %v, %v", name, results, err) } - results := []uint64{0} - fn(host, params, results) return wagonet.Status(wago.AsI32(results[0])) } diff --git a/tcp_test.go b/tcp_test.go index 50d96f2..5665917 100644 --- a/tcp_test.go +++ b/tcp_test.go @@ -20,17 +20,21 @@ import ( "github.com/wago-org/net/internal/resource" wago "github.com/wago-org/wago" "github.com/wago-org/wago/src/core/compiler/wasm" - "github.com/wago-org/wago/testutil/wasmtest" + "github.com/wago-org/wago/tests/wasmtest" ) func TestTCPBindingsAreRegisteredOnlyAsCompleteTable(t *testing.T) { extension := Init(guestTCPConfig(1, 2)) - runtime := runtimeForExtension(t, extension) + runtime := runtimeForNetwork(t, extension) if got := len(extension.tcpBindings()); got != 11 { t.Fatalf("complete checked TCP bindings = %d, want 11", got) } for _, binding := range extension.tcpBindings() { - if _, ok := runtime.HostImports()[TCPModule+"."+binding.name].(wago.HostFunc); !ok { + found := false + for _, spec := range runtime.ProvidedImports() { + found = found || spec.Module == TCPModule && spec.Name == binding.name + } + if !found { t.Fatalf("registered TCP binding %q missing", binding.name) } } @@ -53,25 +57,30 @@ func TestTCPBindingsAreRegisteredOnlyAsCompleteTable(t *testing.T) { func TestGuestTCPUnavailableNamespaceIsTruthful(t *testing.T) { extension := Init(Config{}) - runtime := runtimeForExtension(t, extension) - instance, err := runtime.Instantiate(context.Background(), emptyModule(t, runtime)) + runtime := runtimeForNetwork(t, extension) + module, err := compileImportHarness(runtime, TCPModule) + if err != nil { + t.Fatal(err) + } + instance, err := runtime.Instantiate(context.Background(), module) if err != nil { t.Fatalf("Instantiate: %v", err) } defer instance.Close() - host := udpHostModule{instance: instance, memory: bytes.Repeat([]byte{0x5a}, 16)} - before := append([]byte(nil), host.memory...) + host := udpHostModule{instance: instance, memory: instance.Memory().Bytes()} + copy(host.memory[:16], bytes.Repeat([]byte{0x5a}, 16)) + before := append([]byte(nil), host.memory[:16]...) if got := callRegisteredTCP(t, runtime, "namespace_default", host, 0); got != StatusNotSupported { t.Fatalf("TCP namespace without configuration = %v", got) } - if !bytes.Equal(host.memory, before) { + if !bytes.Equal(host.memory[:16], before) { t.Fatal("unavailable TCP namespace mutated output") } } func TestGuestTCPImportCapabilityGate(t *testing.T) { extension := Init(Config{}) - runtime := runtimeForExtension(t, extension) + runtime := runtimeForNetwork(t, extension) importEntry := append(append(wasmtest.Name(TCPModule), wasmtest.Name("namespace_default")...), 0x00, 0x00) wasmBytes := wasmtest.Module( wasmtest.Section(1, wasmtest.Vec(wasmtest.FuncType([]wasm.ValType{wasm.I32}, []wasm.ValType{wasm.I32}))), @@ -96,8 +105,8 @@ func TestRegisteredGuestTCPTwoNamespaceExchange(t *testing.T) { serverExt, serverRuntime, serverInstance, serverHost := newGuestTCPInstance(t, 4, 3) defer clientInstance.Close() defer serverInstance.Close() - clientState, _ := clientExt.instanceManager().ForInstance(clientInstance) - serverState, _ := serverExt.instanceManager().ForInstance(serverInstance) + clientState, _ := clientExt.instanceManager().SingleState() + serverState, _ := serverExt.instanceManager().SingleState() if got := callRegisteredTCP(t, clientRuntime, "namespace_default", clientHost, 0); got != StatusOK { t.Fatalf("client namespace = %v", got) @@ -179,7 +188,7 @@ func TestGuestTCPCreationValidatesMemoryPolicyQuotaAndKinds(t *testing.T) { config.Limits = &limits extension, _, instance, host := instantiateGuestTCP(t, config) defer instance.Close() - state, _ := extension.instanceManager().ForInstance(instance) + state, _ := extension.instanceManager().SingleState() before := append([]byte(nil), host.memory...) if got := callTCP(t, extension, "namespace_default", host, uint64(len(host.memory)-4)); got != StatusInvalidArgument { @@ -236,8 +245,8 @@ func TestGuestTCPConnectProgressEndpointsAndInstanceIsolation(t *testing.T) { serverExt, _, serverInstance, serverHost := newGuestTCPInstance(t, 22, 21) defer clientInstance.Close() defer serverInstance.Close() - clientState, _ := clientExt.instanceManager().ForInstance(clientInstance) - serverState, _ := serverExt.instanceManager().ForInstance(serverInstance) + clientState, _ := clientExt.instanceManager().SingleState() + serverState, _ := serverExt.instanceManager().SingleState() clientNamespace := guestTCPNamespace(t, clientExt, clientHost) serverNamespace := guestTCPNamespace(t, serverExt, serverHost) @@ -296,12 +305,12 @@ func TestGuestTCPConnectRollsBackInvalidBackendDescriptor(t *testing.T) { if err != nil { t.Fatalf("manager: %v", err) } - extension := &Extension{instances: manager} + extension := &Network{instances: manager} if err := extension.registerTCPModule(); err != nil { t.Fatalf("register TCP module: %v", err) } - runtime := runtimeForExtension(t, extension) - module, err := runtime.Compile([]byte{0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00}) + runtime := runtimeForNetwork(t, extension) + module, err := compileImportHarness(runtime, TCPModule) if err != nil { t.Fatalf("compile: %v", err) } @@ -310,7 +319,7 @@ func TestGuestTCPConnectRollsBackInvalidBackendDescriptor(t *testing.T) { t.Fatalf("instantiate: %v", err) } defer inst.Close() - host := udpHostModule{instance: inst, memory: make([]byte, 256)} + host := udpHostModule{instance: inst, memory: inst.Memory().Bytes()} namespaceHandle := guestTCPNamespace(t, extension, host) encodeGuestEndpoint(t, host.memory, 0, endpointFor(31, 4631)) before := append([]byte(nil), host.memory...) @@ -320,7 +329,7 @@ func TestGuestTCPConnectRollsBackInvalidBackendDescriptor(t *testing.T) { if !bytes.Equal(host.memory, before) { t.Fatal("invalid backend descriptor mutated output") } - state, _ := manager.ForInstance(inst) + state, _ := manager.SingleState() if state.Resources().Len() != 1 || stream.closed.Load() != 1 { t.Fatalf("failed descriptor rollback resources=%d closes=%d", state.Resources().Len(), stream.closed.Load()) } @@ -331,8 +340,8 @@ func TestGuestTCPAcceptPartialIOShutdownCloseAndPoll(t *testing.T) { serverExt, _, serverInstance, serverHost := newGuestTCPInstance(t, 42, 41) defer clientInstance.Close() defer serverInstance.Close() - clientState, _ := clientExt.instanceManager().ForInstance(clientInstance) - serverState, _ := serverExt.instanceManager().ForInstance(serverInstance) + clientState, _ := clientExt.instanceManager().SingleState() + serverState, _ := serverExt.instanceManager().SingleState() clientNamespace := guestTCPNamespace(t, clientExt, clientHost) serverNamespace := guestTCPNamespace(t, serverExt, serverHost) serverEndpoint := endpointFor(42, 4642) @@ -474,12 +483,12 @@ func TestGuestTCPAcceptRollsBackInvalidDescriptor(t *testing.T) { if err != nil { t.Fatalf("manager: %v", err) } - extension := &Extension{instances: manager} + extension := &Network{instances: manager} if err := extension.registerTCPModule(); err != nil { t.Fatalf("register TCP module: %v", err) } - runtime := runtimeForExtension(t, extension) - module, err := runtime.Compile([]byte{0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00}) + runtime := runtimeForNetwork(t, extension) + module, err := compileImportHarness(runtime, TCPModule) if err != nil { t.Fatalf("compile: %v", err) } @@ -488,8 +497,9 @@ func TestGuestTCPAcceptRollsBackInvalidDescriptor(t *testing.T) { t.Fatalf("instantiate: %v", err) } defer inst.Close() - host := udpHostModule{instance: inst, memory: bytes.Repeat([]byte{0x5a}, 256)} - state, _ := manager.ForInstance(inst) + host := udpHostModule{instance: inst, memory: inst.Memory().Bytes()} + copy(host.memory[:256], bytes.Repeat([]byte{0x5a}, 256)) + state, _ := manager.SingleState() listenerHandle, err := state.Resources().Add(resource.KindTCPListener, listener) if err != nil { t.Fatalf("add listener: %v", err) @@ -511,32 +521,28 @@ func TestGuestTCPStreamOperationsRaceClose(t *testing.T) { t.Fatalf("connect = %v", got) } stream, _, _ := decodeGuestTCPStream(t, host.memory, 64) - readFn := findTCPBinding(extension, "read") - writeFn := findTCPBinding(extension, "write") - finishFn := findTCPBinding(extension, "finish_connect") - shutdownFn := findTCPBinding(extension, "shutdown_write") - closeFn := findTCPBinding(extension, "close_stream") - if readFn == nil || writeFn == nil || finishFn == nil || shutdownFn == nil || closeFn == nil { - t.Fatal("missing TCP race binding") - } var wait sync.WaitGroup var bad atomic.Int32 for worker := range 8 { wait.Add(1) go func(worker int) { defer wait.Done() - local := udpHostModule{instance: instance, memory: make([]byte, 128)} for iteration := range 100 { - results := []uint64{0} + var results []uint64 + var err error switch (worker + iteration) % 4 { case 0: - readFn(local, []uint64{uint64(stream), 0, 8, 32}, results) + results, err = instance.Invoke("read", uint64(stream), 0, 8, 32) case 1: - writeFn(local, []uint64{uint64(stream), 0, 8, 32}, results) + results, err = instance.Invoke("write", uint64(stream), 0, 8, 32) case 2: - finishFn(local, []uint64{uint64(stream)}, results) + results, err = instance.Invoke("finish_connect", uint64(stream)) case 3: - shutdownFn(local, []uint64{uint64(stream)}, results) + results, err = instance.Invoke("shutdown_write", uint64(stream)) + } + if err != nil || len(results) != 1 { + bad.Add(1) + return } status := Status(wago.AsI32(results[0])) if status != StatusOK && status != StatusAgain && status != StatusInProgress && status != StatusEOF && status != StatusBadHandle && status != StatusInvalidState && status != StatusConnectionRefused && status != StatusConnectionAborted && status != StatusConnectionBroken { @@ -549,8 +555,11 @@ func TestGuestTCPStreamOperationsRaceClose(t *testing.T) { wait.Add(1) go func() { defer wait.Done() - results := []uint64{0} - closeFn(udpHostModule{instance: instance, memory: make([]byte, 8)}, []uint64{uint64(stream)}, results) + results, err := instance.Invoke("close_stream", uint64(stream)) + if err != nil || len(results) != 1 { + bad.Add(1) + return + } status := Status(wago.AsI32(results[0])) if status != StatusOK && status != StatusBadHandle { bad.Add(1) @@ -611,16 +620,16 @@ func BenchmarkGuestTCPPoll(b *testing.B) { } } -func newGuestTCPInstance(t testing.TB, localLast, gatewayLast byte) (*Extension, *wago.Runtime, *wago.Instance, udpHostModule) { +func newGuestTCPInstance(t testing.TB, localLast, gatewayLast byte) (*Network, *wago.Runtime, *wago.Instance, udpHostModule) { t.Helper() return instantiateGuestTCP(t, guestTCPConfig(localLast, gatewayLast)) } -func instantiateGuestTCP(t testing.TB, config Config) (*Extension, *wago.Runtime, *wago.Instance, udpHostModule) { +func instantiateGuestTCP(t testing.TB, config Config) (*Network, *wago.Runtime, *wago.Instance, udpHostModule) { t.Helper() extension := Init(config) - runtime := runtimeForExtension(t, extension) - module, err := runtime.Compile([]byte{0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00}) + runtime := runtimeForNetwork(t, extension) + module, err := compileImportHarness(runtime, TCPModule) if err != nil { t.Fatalf("Compile empty TCP guest: %v", err) } @@ -628,7 +637,7 @@ func instantiateGuestTCP(t testing.TB, config Config) (*Extension, *wago.Runtime if err != nil { t.Fatalf("Instantiate TCP guest: %v", err) } - return extension, runtime, instance, udpHostModule{instance: instance, memory: make([]byte, 1024)} + return extension, runtime, instance, udpHostModule{instance: instance, memory: instance.Memory().Bytes()} } func guestTCPConfig(localLast, gatewayLast byte) Config { @@ -651,7 +660,7 @@ func guestTCPConfig(localLast, gatewayLast byte) Config { } } -func guestTCPNamespace(t testing.TB, extension *Extension, host udpHostModule) resource.Handle { +func guestTCPNamespace(t testing.TB, extension *Network, host udpHostModule) resource.Handle { t.Helper() if got := callTCP(t, extension, "namespace_default", host, 0); got != StatusOK { t.Fatalf("TCP namespace_default = %v", got) @@ -659,7 +668,7 @@ func guestTCPNamespace(t testing.TB, extension *Extension, host udpHostModule) r return resource.Handle(binary.LittleEndian.Uint64(host.memory[:8])) } -func guestTCPListen(t testing.TB, extension *Extension, host udpHostModule, namespaceHandle resource.Handle, local namespace.Endpoint, out uint32) resource.Handle { +func guestTCPListen(t testing.TB, extension *Network, host udpHostModule, namespaceHandle resource.Handle, local namespace.Endpoint, out uint32) resource.Handle { t.Helper() encodeGuestEndpoint(t, host.memory, 0, local) if got := callTCP(t, extension, "listen", host, uint64(namespaceHandle), 0, uint64(out)); got != StatusOK { @@ -668,19 +677,24 @@ func guestTCPListen(t testing.TB, extension *Extension, host udpHostModule, name return resource.Handle(binary.LittleEndian.Uint64(host.memory[out : out+abi.HandleV1Size])) } -func callRegisteredTCP(t testing.TB, runtime *wago.Runtime, name string, host udpHostModule, params ...uint64) Status { +func callRegisteredTCP(t testing.TB, _ *wago.Runtime, name string, host udpHostModule, params ...uint64) Status { t.Helper() - fn, ok := runtime.HostImports()[TCPModule+"."+name].(wago.HostFunc) - if !ok { - t.Fatalf("registered TCP binding %q missing", name) + results, err := host.instance.Invoke(name, params...) + if err != nil || len(results) != 1 { + t.Fatalf("registered TCP binding %q = %v, %v", name, results, err) } - results := []uint64{0} - fn(host, params, results) return Status(wago.AsI32(results[0])) } -func callTCP(t testing.TB, extension *Extension, name string, host udpHostModule, params ...uint64) Status { +func callTCP(t testing.TB, extension *Network, name string, host udpHostModule, params ...uint64) Status { t.Helper() + if host.instance != nil && host.instance.Memory() != nil { + results, err := host.instance.Invoke(name, params...) + if err != nil || len(results) != 1 { + t.Fatalf("TCP import %q = %v, %v", name, results, err) + } + return Status(wago.AsI32(results[0])) + } fn := findTCPBinding(extension, name) if fn == nil { t.Fatalf("TCP binding %q missing", name) @@ -690,7 +704,7 @@ func callTCP(t testing.TB, extension *Extension, name string, host udpHostModule return Status(wago.AsI32(results[0])) } -func findTCPBinding(extension *Extension, name string) wago.HostFunc { +func findTCPBinding(extension *Network, name string) wago.HostFunc { for _, candidate := range extension.tcpBindings() { if candidate.name == name { return candidate.fn diff --git a/tls/plugin_test.go b/tls/plugin_test.go new file mode 100644 index 0000000..2286b14 --- /dev/null +++ b/tls/plugin_test.go @@ -0,0 +1,15 @@ +package tls_test + +import ( + wagonet "github.com/wago-org/net" + "github.com/wago-org/net/internal/plugintest" + wago "github.com/wago-org/wago" +) + +func loadNetwork(runtime *wago.Runtime, network *wagonet.Network) error { + return plugintest.LoadNetwork(runtime, network) +} + +func compileImportHarness(runtime *wago.Runtime) (*wago.Module, error) { + return plugintest.CompileImportHarness(runtime, wagonet.TLSModule) +} diff --git a/tls/register_test.go b/tls/register_test.go index 7c09464..9278114 100644 --- a/tls/register_test.go +++ b/tls/register_test.go @@ -24,7 +24,7 @@ func TestRegisterExposesOnlyTLSAndSharedCore(t *testing.T) { t.Fatal(err) } runtime := wago.NewRuntime() - if err := runtime.Use(network); err != nil { + if err := loadNetwork(runtime, network); err != nil { t.Fatal(err) } if got, want := runtime.Capabilities(), []wago.Capability{wagonet.CapInfo, wagonet.CapTLS}; !reflect.DeepEqual(got, want) { @@ -52,7 +52,7 @@ func TestTCPAndTLSComposeWithoutCapabilityWidening(t *testing.T) { t.Fatal(err) } runtime := wago.NewRuntime() - if err := runtime.Use(network); err != nil { + if err := loadNetwork(runtime, network); err != nil { t.Fatal(err) } wantCapabilities := []wago.Capability{wagonet.CapInfo, wagonet.CapTCP, wagonet.CapTLS} @@ -92,10 +92,10 @@ func TestPublicTLSRegistrationLoopbackOptionControlsOnlyTLSConnect(t *testing.T) t.Fatal(err) } runtime := wago.NewRuntime() - if err := runtime.Use(network); err != nil { + if err := loadNetwork(runtime, network); err != nil { t.Fatal(err) } - module, err := runtime.Compile([]byte{0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00}) + module, err := compileImportHarness(runtime) if err != nil { t.Fatal(err) } @@ -104,7 +104,7 @@ func TestPublicTLSRegistrationLoopbackOptionControlsOnlyTLSConnect(t *testing.T) t.Fatal(err) } defer instance.Close() - host := tlsExactHost{instance: instance, memory: make([]byte, 256)} + host := tlsExactHost{instance: instance, memory: instance.Memory().Bytes()} if got := callTLS(t, runtime, host, "namespace_default", 0); got != wagonet.StatusOK { t.Fatalf("namespace_default = %v", got) } @@ -145,7 +145,7 @@ func TestRegisterRejectsMissingProfileDuplicateAndFrozen(t *testing.T) { t.Fatalf("duplicate = %v", err) } runtime := wago.NewRuntime() - if err := runtime.Use(network); err != nil { + if err := loadNetwork(runtime, network); err != nil { t.Fatal(err) } if err := wagonettls.Register(network, wagonettls.WithClientProfile(profile)); !errors.Is(err, wagonet.ErrProtocolRegistrationFrozen) { @@ -161,14 +161,12 @@ type tlsExactHost struct { func (host tlsExactHost) Memory() []byte { return host.memory } func (host tlsExactHost) Instance() *wago.Instance { return host.instance } -func callTLS(t testing.TB, runtime *wago.Runtime, host tlsExactHost, name string, params ...uint64) wagonet.Status { +func callTLS(t testing.TB, _ *wago.Runtime, host tlsExactHost, name string, params ...uint64) wagonet.Status { t.Helper() - function, ok := runtime.HostImports()[wagonet.TLSModule+"."+name].(wago.HostFunc) - if !ok { - t.Fatalf("TLS import %q missing", name) + results, err := host.instance.Invoke(name, params...) + if err != nil || len(results) != 1 { + t.Fatalf("TLS import %q = %v, %v", name, results, err) } - results := []uint64{0} - function(host, params, results) return wagonet.Status(wago.AsI32(results[0])) } diff --git a/udp/plugin_test.go b/udp/plugin_test.go new file mode 100644 index 0000000..b562238 --- /dev/null +++ b/udp/plugin_test.go @@ -0,0 +1,19 @@ +package udp_test + +import ( + wagonet "github.com/wago-org/net" + "github.com/wago-org/net/internal/plugintest" + wago "github.com/wago-org/wago" +) + +func loadNetwork(runtime *wago.Runtime, network *wagonet.Network) error { + return plugintest.LoadNetwork(runtime, network) +} + +func hasImport(runtime *wago.Runtime, module, name string) bool { + return plugintest.HasImport(runtime, module, name) +} + +func compileImportHarness(runtime *wago.Runtime) (*wago.Module, error) { + return plugintest.CompileImportHarness(runtime, wagonet.UDPModule) +} diff --git a/udp/register/register.go b/udp/register/register.go index 772deb4..e7aeae6 100644 --- a/udp/register/register.go +++ b/udp/register/register.go @@ -1,5 +1,4 @@ -// Package register self-registers the selective UDP-only Wago networking -// extension for custom binaries built by `wago pkg build`. +// Package register exposes the explicit UDP-only networking catalog entry. package register import ( @@ -8,12 +7,15 @@ import ( wago "github.com/wago-org/wago" ) -func init() { - wago.RegisterExtension("net-udp", func() wago.Extension { - network := wagonet.New() - if err := udp.Register(network); err != nil { - panic("wagonet/udp/register: " + err.Error()) - } - return network +func Provider() wago.PluginProvider { + return wagonet.Provider(wagonet.ProviderSpec{ + ID: "github.com/wago-org/net/udp", Name: "Wago UDP", Description: "Bounded checked UDP networking.", + Modules: []string{wagonet.Module, wagonet.UDPModule}, + Factory: func() (*wagonet.Network, error) { + network := wagonet.New() + return network, udp.Register(network) + }, }) } + +func Providers() []wago.PluginProvider { return []wago.PluginProvider{Provider()} } diff --git a/udp/register/register_test.go b/udp/register/register_test.go index 4cbe7c0..1693035 100644 --- a/udp/register/register_test.go +++ b/udp/register/register_test.go @@ -6,20 +6,23 @@ import ( "testing" wagonet "github.com/wago-org/net" - _ "github.com/wago-org/net/udp/register" + "github.com/wago-org/net/internal/plugintest" + netregister "github.com/wago-org/net/udp/register" wago "github.com/wago-org/wago" "github.com/wago-org/wago/src/core/compiler/wasm" - "github.com/wago-org/wago/testutil/wasmtest" + "github.com/wago-org/wago/tests/wasmtest" ) func TestUDPFactoryHasExactRuntimeSurface(t *testing.T) { - extension, ok := wago.NewExtension("net-udp") - if !ok { - t.Fatal("UDP-only extension was not registered") + providers := netregister.Providers() + if len(providers) != 1 { + t.Fatalf("Providers = %d, want 1", len(providers)) } + provider := providers[0] runtime := wago.NewRuntime() - if err := runtime.Use(extension); err != nil { - t.Fatalf("Use: %v", err) + defer runtime.Close() + if err := runtime.LoadPlugins(context.Background(), plugintest.Set(provider)); err != nil { + t.Fatalf("LoadPlugins: %v", err) } wantCapabilities := []wago.Capability{wagonet.CapInfo, wagonet.CapUDP} if got := runtime.Capabilities(); !reflect.DeepEqual(got, wantCapabilities) { diff --git a/udp/register_test.go b/udp/register_test.go index d2dc140..f90c3c5 100644 --- a/udp/register_test.go +++ b/udp/register_test.go @@ -14,7 +14,7 @@ import ( "github.com/wago-org/net/udp" wago "github.com/wago-org/wago" "github.com/wago-org/wago/src/core/compiler/wasm" - "github.com/wago-org/wago/testutil/wasmtest" + "github.com/wago-org/wago/tests/wasmtest" ) func TestRegisterExposesOnlyUDPAndSharedCore(t *testing.T) { @@ -24,7 +24,7 @@ func TestRegisterExposesOnlyUDPAndSharedCore(t *testing.T) { } runtime := wago.NewRuntime() - if err := runtime.Use(network); err != nil { + if err := loadNetwork(runtime, network); err != nil { t.Fatalf("Use: %v", err) } wantCapabilities := []wago.Capability{wagonet.CapInfo, wagonet.CapUDP} @@ -39,10 +39,10 @@ func TestRegisterExposesOnlyUDPAndSharedCore(t *testing.T) { if !reflect.DeepEqual(imports, wantImports) { t.Fatalf("import modules = %v, want %v", imports, wantImports) } - if _, ok := runtime.HostImports()[wagonet.TCPModule+".namespace_default"]; ok { + if hasImport(runtime, wagonet.TCPModule, "namespace_default") { t.Fatal("TCP import exposed by UDP-only registration") } - if _, ok := runtime.HostImports()[wagonet.DNSModule+".namespace_default"]; ok { + if hasImport(runtime, wagonet.DNSModule, "namespace_default") { t.Fatal("DNS import exposed by UDP-only registration") } } @@ -66,7 +66,7 @@ func TestRegisterRejectsDuplicateInvalidOptionFrozenAndNilNetwork(t *testing.T) t.Fatalf("duplicate registration = %v", err) } runtime := wago.NewRuntime() - if err := runtime.Use(network); err != nil { + if err := loadNetwork(runtime, network); err != nil { t.Fatalf("Use: %v", err) } if err := udp.Register(network); !errors.Is(err, wagonet.ErrProtocolRegistrationFrozen) { @@ -80,29 +80,12 @@ func TestSelectiveUDPBindingUsesExactSharedInstanceState(t *testing.T) { t.Fatalf("Register: %v", err) } runtime := wago.NewRuntime() - if err := runtime.Use(network); err != nil { + if err := loadNetwork(runtime, network); err != nil { t.Fatalf("Use: %v", err) } - module, err := runtime.Compile([]byte{0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00}) - if err != nil { - t.Fatalf("Compile empty module: %v", err) - } - instance, err := runtime.Instantiate(context.Background(), module) - if err != nil { - t.Fatalf("Instantiate: %v", err) - } - defer instance.Close() - - fn, ok := runtime.HostImports()[wagonet.UDPModule+".namespace_default"].(wago.HostFunc) - if !ok { + if !hasImport(runtime, wagonet.UDPModule, "namespace_default") { t.Fatal("selective UDP namespace binding missing") } - host := exactHost{instance: instance, memory: make([]byte, wagonet.HandleV1Size)} - results := []uint64{0} - fn(host, []uint64{0}, results) - if got := wagonet.Status(wago.AsI32(results[0])); got != wagonet.StatusNotSupported { - t.Fatalf("namespace_default without configured namespace = %v", got) - } } func TestDefaultUDPAllowsEphemeralUnicastAndDeniesServerSpecialAndCallerDeniedAuthority(t *testing.T) { @@ -118,10 +101,10 @@ func TestDefaultUDPAllowsEphemeralUnicastAndDeniesServerSpecialAndCallerDeniedAu t.Fatalf("Register: %v", err) } runtime := wago.NewRuntime() - if err := runtime.Use(network); err != nil { + if err := loadNetwork(runtime, network); err != nil { t.Fatalf("Use: %v", err) } - module, err := runtime.Compile([]byte{0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00}) + module, err := compileImportHarness(runtime) if err != nil { t.Fatal(err) } @@ -130,7 +113,7 @@ func TestDefaultUDPAllowsEphemeralUnicastAndDeniesServerSpecialAndCallerDeniedAu t.Fatalf("Instantiate: %v", err) } defer instance.Close() - host := exactHost{instance: instance, memory: make([]byte, 256)} + host := exactHost{instance: instance, memory: instance.Memory().Bytes()} if got := callUDP(t, runtime, host, "namespace_default", 0); got != wagonet.StatusOK { t.Fatalf("namespace_default = %v", got) } @@ -175,10 +158,10 @@ func TestDefaultUDPStorageFitsSharedDefaultsAndStopsAtEightSockets(t *testing.T) t.Fatalf("Register: %v", err) } runtime := wago.NewRuntime() - if err := runtime.Use(network); err != nil { + if err := loadNetwork(runtime, network); err != nil { t.Fatalf("Use: %v", err) } - module, err := runtime.Compile([]byte{0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00}) + module, err := compileImportHarness(runtime) if err != nil { t.Fatal(err) } @@ -187,7 +170,7 @@ func TestDefaultUDPStorageFitsSharedDefaultsAndStopsAtEightSockets(t *testing.T) t.Fatalf("Instantiate: %v", err) } defer instance.Close() - host := exactHost{instance: instance, memory: make([]byte, 256)} + host := exactHost{instance: instance, memory: instance.Memory().Bytes()} if got := callUDP(t, runtime, host, "namespace_default", 0); got != wagonet.StatusOK { t.Fatalf("namespace_default = %v", got) } @@ -211,7 +194,7 @@ func TestUDPRegistrationLeavesTCPAndDNSImportsUnresolved(t *testing.T) { t.Fatalf("Register: %v", err) } runtime := wago.NewRuntime() - if err := runtime.Use(network); err != nil { + if err := loadNetwork(runtime, network); err != nil { t.Fatalf("Use: %v", err) } @@ -244,14 +227,12 @@ type exactHost struct { func (h exactHost) Memory() []byte { return h.memory } func (h exactHost) Instance() *wago.Instance { return h.instance } -func callUDP(t testing.TB, runtime *wago.Runtime, host exactHost, name string, params ...uint64) wagonet.Status { +func callUDP(t testing.TB, _ *wago.Runtime, host exactHost, name string, params ...uint64) wagonet.Status { t.Helper() - fn, ok := runtime.HostImports()[wagonet.UDPModule+"."+name].(wago.HostFunc) - if !ok { - t.Fatalf("UDP import %q missing", name) + results, err := host.instance.Invoke(name, params...) + if err != nil || len(results) != 1 { + t.Fatalf("UDP import %q = %v, %v", name, results, err) } - results := []uint64{0} - fn(host, params, results) return wagonet.Status(wago.AsI32(results[0])) } diff --git a/udp_test.go b/udp_test.go index 62f564c..eda6099 100644 --- a/udp_test.go +++ b/udp_test.go @@ -16,7 +16,7 @@ import ( "github.com/wago-org/net/internal/resource" wago "github.com/wago-org/wago" "github.com/wago-org/wago/src/core/compiler/wasm" - "github.com/wago-org/wago/testutil/wasmtest" + "github.com/wago-org/wago/tests/wasmtest" ) type udpHostModule struct { @@ -29,25 +29,30 @@ func (m udpHostModule) Instance() *wago.Instance { return m.instance } func TestGuestUDPUnavailableNamespaceIsTruthful(t *testing.T) { extension := Init(Config{}) - runtime := runtimeForExtension(t, extension) - instance, err := runtime.Instantiate(context.Background(), emptyModule(t, runtime)) + runtime := runtimeForNetwork(t, extension) + module, err := compileImportHarness(runtime, UDPModule) + if err != nil { + t.Fatal(err) + } + instance, err := runtime.Instantiate(context.Background(), module) if err != nil { t.Fatalf("Instantiate: %v", err) } defer instance.Close() - host := udpHostModule{instance: instance, memory: bytes.Repeat([]byte{0x5a}, 16)} - before := append([]byte(nil), host.memory...) + host := udpHostModule{instance: instance, memory: instance.Memory().Bytes()} + copy(host.memory[:16], bytes.Repeat([]byte{0x5a}, 16)) + before := append([]byte(nil), host.memory[:16]...) if got := callUDP(t, extension, "namespace_default", host, 0); got != StatusNotSupported { t.Fatalf("namespace without configuration = %v", got) } - if !bytes.Equal(host.memory, before) { + if !bytes.Equal(host.memory[:16], before) { t.Fatal("unavailable namespace mutated output") } } func TestGuestUDPImportCapabilityGate(t *testing.T) { extension := Init(Config{}) - runtime := runtimeForExtension(t, extension) + runtime := runtimeForNetwork(t, extension) importEntry := append(append(wasmtest.Name(UDPModule), wasmtest.Name("namespace_default")...), 0x00, 0x00) wasm := wasmtest.Module( wasmtest.Section(1, wasmtest.Vec(wasmtest.FuncType([]wasm.ValType{wasm.I32}, []wasm.ValType{wasm.I32}))), @@ -74,7 +79,7 @@ func TestGuestUDPImportsIsolationPolicyQuotaAndClose(t *testing.T) { defer secondInstance.Close() firstNamespace := guestNamespace(t, firstRuntime, firstHost) - secondNamespace := guestNamespaceFromExtension(t, secondExt, secondHost) + secondNamespace := guestNamespaceFromNetwork(t, secondExt, secondHost) firstSocket := guestBind(t, firstExt, firstHost, firstNamespace, endpointFor(1, 4101), 64) secondSocket := guestBind(t, secondExt, secondHost, secondNamespace, endpointFor(2, 4102), 64) @@ -126,11 +131,11 @@ func TestGuestUDPEmptyTruncationAndFailedMemoryWrites(t *testing.T) { receiverExt, _, receiverInstance, receiverHost := newGuestUDPInstance(t, 12, 11) defer senderInstance.Close() defer receiverInstance.Close() - senderState, _ := senderExt.instanceManager().ForInstance(senderInstance) - receiverState, _ := receiverExt.instanceManager().ForInstance(receiverInstance) + senderState, _ := senderExt.instanceManager().SingleState() + receiverState, _ := receiverExt.instanceManager().SingleState() - senderNamespace := guestNamespaceFromExtension(t, senderExt, senderHost) - receiverNamespace := guestNamespaceFromExtension(t, receiverExt, receiverHost) + senderNamespace := guestNamespaceFromNetwork(t, senderExt, senderHost) + receiverNamespace := guestNamespaceFromNetwork(t, receiverExt, receiverHost) sender := guestBind(t, senderExt, senderHost, senderNamespace, endpointFor(11, 4211), 64) receiver := guestBind(t, receiverExt, receiverHost, receiverNamespace, endpointFor(12, 4212), 64) @@ -193,8 +198,8 @@ func TestGuestUDPEmptyTruncationAndFailedMemoryWrites(t *testing.T) { func TestGuestUDPBindValidatesOutputBeforeAllocation(t *testing.T) { extension, _, instance, host := newGuestUDPInstance(t, 21, 22) defer instance.Close() - state, _ := extension.instanceManager().ForInstance(instance) - namespaceHandle := guestNamespaceFromExtension(t, extension, host) + state, _ := extension.instanceManager().SingleState() + namespaceHandle := guestNamespaceFromNetwork(t, extension, host) encodeGuestEndpoint(t, host.memory, 0, endpointFor(21, 4321)) if got := callUDP(t, extension, "bind", host, uint64(namespaceHandle), 0, uint64(len(host.memory)-4)); got != StatusInvalidArgument { t.Fatalf("bad output bind = %v", got) @@ -208,7 +213,7 @@ func TestGuestUDPBindValidatesOutputBeforeAllocation(t *testing.T) { } } -func newGuestUDPInstance(t testing.TB, localLast, gatewayLast byte) (*Extension, *wago.Runtime, *wago.Instance, udpHostModule) { +func newGuestUDPInstance(t testing.TB, localLast, gatewayLast byte) (*Network, *wago.Runtime, *wago.Instance, udpHostModule) { t.Helper() limits := QuotaLimits{Resources: 2, UDPResources: 1, QueuedBytes: 128, ServiceUnits: 32} ready := ReadinessConfig{MaxRegistrations: 2} @@ -227,8 +232,8 @@ func newGuestUDPInstance(t testing.TB, localLast, gatewayLast byte) (*Extension, UDP: UDPConfig{MaxSockets: 1, ReceiveBytes: 64, TransmitBytes: 64, ReceiveDatagrams: 2, TransmitDatagrams: 2, MaxPayloadBytes: 32}, }, }) - runtime := runtimeForExtension(t, extension) - module, err := runtime.Compile([]byte{0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00}) + runtime := runtimeForNetwork(t, extension) + module, err := compileImportHarness(runtime, UDPModule) if err != nil { t.Fatalf("Compile empty UDP guest: %v", err) } @@ -236,33 +241,31 @@ func newGuestUDPInstance(t testing.TB, localLast, gatewayLast byte) (*Extension, if err != nil { t.Fatalf("Instantiate UDP guest: %v", err) } - return extension, runtime, instance, udpHostModule{instance: instance, memory: make([]byte, 512)} + return extension, runtime, instance, udpHostModule{instance: instance, memory: instance.Memory().Bytes()} } -func runtimeForExtension(t testing.TB, extension *Extension) *wago.Runtime { +func runtimeForNetwork(t testing.TB, extension *Network) *wago.Runtime { t.Helper() runtime := wago.NewRuntime() - if err := runtime.Use(extension); err != nil { + if err := loadNetwork(runtime, extension); err != nil { t.Fatalf("Use UDP extension: %v", err) } return runtime } -func guestNamespace(t testing.TB, runtime *wago.Runtime, host udpHostModule) resource.Handle { +func guestNamespace(t testing.TB, _ *wago.Runtime, host udpHostModule) resource.Handle { t.Helper() - fn, ok := runtime.HostImports()[UDPModule+".namespace_default"].(wago.HostFunc) - if !ok { - t.Fatal("namespace_default binding missing") + results, err := host.instance.Invoke("namespace_default", 0) + if err != nil || len(results) != 1 { + t.Fatalf("namespace_default = %v, %v", results, err) } - results := []uint64{0} - fn(host, []uint64{0}, results) if got := Status(wago.AsI32(results[0])); got != StatusOK { t.Fatalf("namespace_default = %v", got) } return resource.Handle(binary.LittleEndian.Uint64(host.memory[:8])) } -func guestNamespaceFromExtension(t testing.TB, extension *Extension, host udpHostModule) resource.Handle { +func guestNamespaceFromNetwork(t testing.TB, extension *Network, host udpHostModule) resource.Handle { t.Helper() if got := callUDP(t, extension, "namespace_default", host, 0); got != StatusOK { t.Fatalf("namespace_default = %v", got) @@ -270,7 +273,7 @@ func guestNamespaceFromExtension(t testing.TB, extension *Extension, host udpHos return resource.Handle(binary.LittleEndian.Uint64(host.memory[:8])) } -func guestBind(t testing.TB, extension *Extension, host udpHostModule, namespaceHandle resource.Handle, local namespace.Endpoint, out uint32) resource.Handle { +func guestBind(t testing.TB, extension *Network, host udpHostModule, namespaceHandle resource.Handle, local namespace.Endpoint, out uint32) resource.Handle { t.Helper() encodeGuestEndpoint(t, host.memory, 0, local) if got := callUDP(t, extension, "bind", host, uint64(namespaceHandle), 0, uint64(out)); got != StatusOK { @@ -279,8 +282,15 @@ func guestBind(t testing.TB, extension *Extension, host udpHostModule, namespace return resource.Handle(binary.LittleEndian.Uint64(host.memory[out : out+8])) } -func callUDP(t testing.TB, extension *Extension, name string, host udpHostModule, params ...uint64) Status { +func callUDP(t testing.TB, extension *Network, name string, host udpHostModule, params ...uint64) Status { t.Helper() + if host.instance != nil && host.instance.Memory() != nil { + results, err := host.instance.Invoke(name, params...) + if err != nil || len(results) != 1 { + t.Fatalf("UDP import %q = %v, %v", name, results, err) + } + return Status(wago.AsI32(results[0])) + } var binding wago.HostFunc for _, candidate := range extension.udpBindings() { if candidate.name == name { @@ -347,5 +357,3 @@ func concreteNamespace(t testing.TB, state *instance.State) linkedNamespace { } return backend } - -var _ wago.InstanceHostModule = udpHostModule{} diff --git a/wago.json b/wago.json index 685fc59..522dc87 100644 --- a/wago.json +++ b/wago.json @@ -1,26 +1,30 @@ { - "schema": "wago-plugin/v1", - "module": "github.com/wago-org/net", - "name": "Wago Networking", - "version": "0.0.0", - "description": "Private experimental capability-gated networking plugins for an exactly reviewed Wago source revision, backed initially by lneto.", - "stability": "experimental", - "license": "Apache-2.0", - "homepage": "https://github.com/wago-org/net", - "repository": "https://github.com/wago-org/net", - "authors": ["The wago authors"], - "private": true, - "keywords": [ - "networking", - "tcp", - "udp", - "dns", - "capability-gated", - "wago", - "lneto", - "wasm" - ], - "engines": { - "wago": "0.1.0" + "$schema": "https://wago.sh/v1/schema.json", + "package": { + "module": "github.com/wago-org/net", + "version": "0.1.0", + "name": "Wago Networking", + "description": "Bounded capability-gated TCP, UDP, DNS, and network protocol plugins for Wago.", + "stability": "experimental", + "license": "Apache-2.0", + "homepage": "https://github.com/wago-org/net#readme", + "repository": "https://github.com/wago-org/net", + "category": "networking", + "tags": ["networking", "tcp", "udp", "dns", "capability-gated", "wasm"], + "authors": [{"name": "Wago contributors"}], + "engines": {"wago": ">=0.1.0"}, + "subpackages": [ + {"module": "github.com/wago-org/net/dhcpv4", "name": "Wago DHCPv4", "description": "Bounded checked DHCPv4 networking.", "stability": "experimental", "tags": ["networking", "dhcpv4"]}, + {"module": "github.com/wago-org/net/dhcpv6", "name": "Wago DHCPv6", "description": "Bounded checked initial DHCPv6 acquisition.", "stability": "experimental", "tags": ["networking", "dhcpv6"]}, + {"module": "github.com/wago-org/net/dns", "name": "Wago DNS", "description": "Bounded checked DNS networking.", "stability": "experimental", "tags": ["networking", "dns"]}, + {"module": "github.com/wago-org/net/icmpv4", "name": "Wago ICMPv4", "description": "Bounded checked ICMPv4 networking.", "stability": "experimental", "tags": ["networking", "icmpv4"]}, + {"module": "github.com/wago-org/net/icmpv6", "name": "Wago ICMPv6", "description": "Bounded checked ICMPv6 and neighbor discovery.", "stability": "experimental", "tags": ["networking", "icmpv6"]}, + {"module": "github.com/wago-org/net/ipv6", "name": "Wago IPv6", "description": "Bounded checked IPv6 networking.", "stability": "experimental", "tags": ["networking", "ipv6"]}, + {"module": "github.com/wago-org/net/linklocal4", "name": "Wago IPv4 Link-Local", "description": "Bounded checked IPv4 link-local networking.", "stability": "experimental", "tags": ["networking", "link-local"]}, + {"module": "github.com/wago-org/net/mdns", "name": "Wago mDNS", "description": "Bounded checked multicast DNS networking.", "stability": "experimental", "tags": ["networking", "mdns"]}, + {"module": "github.com/wago-org/net/ntp", "name": "Wago NTP", "description": "Bounded checked NTP networking.", "stability": "experimental", "tags": ["networking", "ntp"]}, + {"module": "github.com/wago-org/net/tcp", "name": "Wago TCP", "description": "Bounded checked TCP networking.", "stability": "experimental", "tags": ["networking", "tcp"]}, + {"module": "github.com/wago-org/net/udp", "name": "Wago UDP", "description": "Bounded checked UDP networking.", "stability": "experimental", "tags": ["networking", "udp"]} + ] } } diff --git a/wago.providers.json b/wago.providers.json new file mode 100644 index 0000000..20b0b32 --- /dev/null +++ b/wago.providers.json @@ -0,0 +1,795 @@ +{ + "$schema": "https://wago.sh/v1/providers.schema.json", + "providers": [ + { + "importPath": "github.com/wago-org/net/register", + "definition": { + "id": "github.com/wago-org/net", + "name": "Wago Networking", + "version": "0.1.0", + "description": "Bounded capability-gated TCP, UDP, DNS, and network protocol plugins for Wago.", + "stability": "experimental", + "compatibility": { + "engines": { + "wago": "\u003e=0.1.0" + } + }, + "provenance": { + "homepage": "https://github.com/wago-org/net#readme", + "repository": "https://github.com/wago-org/net", + "license": "Apache-2.0", + "authors": [ + "Wago contributors" + ] + }, + "authorities": [ + { + "name": "host.caller.identify", + "mode": "required", + "reason": "resolve exact synchronous callers without instance authority", + "scope": {} + }, + { + "name": "host.import.define", + "mode": "required", + "reason": "define the selected checked networking guest imports", + "scope": { + "modules": [ + "wago_net", + "wago_net_dhcpv4", + "wago_net_dhcpv6", + "wago_net_dns", + "wago_net_icmpv4", + "wago_net_icmpv6", + "wago_net_ipv6", + "wago_net_linklocal4", + "wago_net_mdns", + "wago_net_ntp", + "wago_net_tcp", + "wago_net_udp" + ] + } + }, + { + "name": "instance.close.observe", + "mode": "required", + "reason": "detach network state before exact instance resources close", + "scope": {} + }, + { + "name": "instance.instantiate.intercept", + "mode": "required", + "reason": "transactionally attach isolated network state before guest start", + "scope": {} + } + ], + "configSchema": { + "additionalProperties": false, + "type": "object" + }, + "provides": [ + { + "id": "github.com/wago-org/net/service", + "major": 1 + } + ] + }, + "definitionDigest": "sha256:98786de256a4a308f9c8e283da8f4e58561a93b55ce737ad6909e35844fa382f" + }, + { + "importPath": "github.com/wago-org/net/register", + "definition": { + "id": "github.com/wago-org/net/dhcpv4", + "name": "Wago DHCPv4", + "version": "0.1.0", + "description": "Bounded checked DHCPv4 networking.", + "stability": "experimental", + "compatibility": { + "engines": { + "wago": "\u003e=0.1.0" + } + }, + "provenance": { + "homepage": "https://github.com/wago-org/net#readme", + "repository": "https://github.com/wago-org/net", + "license": "Apache-2.0", + "authors": [ + "Wago contributors" + ] + }, + "authorities": [ + { + "name": "host.caller.identify", + "mode": "required", + "reason": "resolve exact synchronous callers without instance authority", + "scope": {} + }, + { + "name": "host.import.define", + "mode": "required", + "reason": "define the selected checked networking guest imports", + "scope": { + "modules": [ + "wago_net", + "wago_net_dhcpv4" + ] + } + }, + { + "name": "instance.close.observe", + "mode": "required", + "reason": "detach network state before exact instance resources close", + "scope": {} + }, + { + "name": "instance.instantiate.intercept", + "mode": "required", + "reason": "transactionally attach isolated network state before guest start", + "scope": {} + } + ], + "configSchema": { + "additionalProperties": false, + "type": "object" + }, + "provides": [ + { + "id": "github.com/wago-org/net/service", + "major": 1 + } + ] + }, + "definitionDigest": "sha256:3040560e5bf628cca179f9c6fdb2f9982f658196f5167bfef3c36e1ae6eedd73" + }, + { + "importPath": "github.com/wago-org/net/register", + "definition": { + "id": "github.com/wago-org/net/dhcpv6", + "name": "Wago DHCPv6", + "version": "0.1.0", + "description": "Bounded checked initial DHCPv6 acquisition.", + "stability": "experimental", + "compatibility": { + "engines": { + "wago": "\u003e=0.1.0" + } + }, + "provenance": { + "homepage": "https://github.com/wago-org/net#readme", + "repository": "https://github.com/wago-org/net", + "license": "Apache-2.0", + "authors": [ + "Wago contributors" + ] + }, + "authorities": [ + { + "name": "host.caller.identify", + "mode": "required", + "reason": "resolve exact synchronous callers without instance authority", + "scope": {} + }, + { + "name": "host.import.define", + "mode": "required", + "reason": "define the selected checked networking guest imports", + "scope": { + "modules": [ + "wago_net", + "wago_net_dhcpv6" + ] + } + }, + { + "name": "instance.close.observe", + "mode": "required", + "reason": "detach network state before exact instance resources close", + "scope": {} + }, + { + "name": "instance.instantiate.intercept", + "mode": "required", + "reason": "transactionally attach isolated network state before guest start", + "scope": {} + } + ], + "configSchema": { + "additionalProperties": false, + "type": "object" + }, + "provides": [ + { + "id": "github.com/wago-org/net/service", + "major": 1 + } + ] + }, + "definitionDigest": "sha256:1dd54e3b8829ed7f478f8a2bd0945c6a4fa73133a5c5fb9feb060ba4462239e2" + }, + { + "importPath": "github.com/wago-org/net/register", + "definition": { + "id": "github.com/wago-org/net/dns", + "name": "Wago DNS", + "version": "0.1.0", + "description": "Bounded checked DNS networking.", + "stability": "experimental", + "compatibility": { + "engines": { + "wago": "\u003e=0.1.0" + } + }, + "provenance": { + "homepage": "https://github.com/wago-org/net#readme", + "repository": "https://github.com/wago-org/net", + "license": "Apache-2.0", + "authors": [ + "Wago contributors" + ] + }, + "authorities": [ + { + "name": "host.caller.identify", + "mode": "required", + "reason": "resolve exact synchronous callers without instance authority", + "scope": {} + }, + { + "name": "host.import.define", + "mode": "required", + "reason": "define the selected checked networking guest imports", + "scope": { + "modules": [ + "wago_net", + "wago_net_dns" + ] + } + }, + { + "name": "instance.close.observe", + "mode": "required", + "reason": "detach network state before exact instance resources close", + "scope": {} + }, + { + "name": "instance.instantiate.intercept", + "mode": "required", + "reason": "transactionally attach isolated network state before guest start", + "scope": {} + } + ], + "configSchema": { + "additionalProperties": false, + "type": "object" + }, + "provides": [ + { + "id": "github.com/wago-org/net/service", + "major": 1 + } + ] + }, + "definitionDigest": "sha256:46daedcec55a4a23d21aa687b6f9d213bff5b6e76939cd6ab37dc471209fec2c" + }, + { + "importPath": "github.com/wago-org/net/register", + "definition": { + "id": "github.com/wago-org/net/icmpv4", + "name": "Wago ICMPv4", + "version": "0.1.0", + "description": "Bounded checked ICMPv4 networking.", + "stability": "experimental", + "compatibility": { + "engines": { + "wago": "\u003e=0.1.0" + } + }, + "provenance": { + "homepage": "https://github.com/wago-org/net#readme", + "repository": "https://github.com/wago-org/net", + "license": "Apache-2.0", + "authors": [ + "Wago contributors" + ] + }, + "authorities": [ + { + "name": "host.caller.identify", + "mode": "required", + "reason": "resolve exact synchronous callers without instance authority", + "scope": {} + }, + { + "name": "host.import.define", + "mode": "required", + "reason": "define the selected checked networking guest imports", + "scope": { + "modules": [ + "wago_net", + "wago_net_icmpv4" + ] + } + }, + { + "name": "instance.close.observe", + "mode": "required", + "reason": "detach network state before exact instance resources close", + "scope": {} + }, + { + "name": "instance.instantiate.intercept", + "mode": "required", + "reason": "transactionally attach isolated network state before guest start", + "scope": {} + } + ], + "configSchema": { + "additionalProperties": false, + "type": "object" + }, + "provides": [ + { + "id": "github.com/wago-org/net/service", + "major": 1 + } + ] + }, + "definitionDigest": "sha256:bd20a4101b2c37f883ba3ead4bd2827b04fe26a861af155046d211d56432bae3" + }, + { + "importPath": "github.com/wago-org/net/register", + "definition": { + "id": "github.com/wago-org/net/icmpv6", + "name": "Wago ICMPv6", + "version": "0.1.0", + "description": "Bounded checked ICMPv6 and neighbor discovery.", + "stability": "experimental", + "compatibility": { + "engines": { + "wago": "\u003e=0.1.0" + } + }, + "provenance": { + "homepage": "https://github.com/wago-org/net#readme", + "repository": "https://github.com/wago-org/net", + "license": "Apache-2.0", + "authors": [ + "Wago contributors" + ] + }, + "authorities": [ + { + "name": "host.caller.identify", + "mode": "required", + "reason": "resolve exact synchronous callers without instance authority", + "scope": {} + }, + { + "name": "host.import.define", + "mode": "required", + "reason": "define the selected checked networking guest imports", + "scope": { + "modules": [ + "wago_net", + "wago_net_icmpv6" + ] + } + }, + { + "name": "instance.close.observe", + "mode": "required", + "reason": "detach network state before exact instance resources close", + "scope": {} + }, + { + "name": "instance.instantiate.intercept", + "mode": "required", + "reason": "transactionally attach isolated network state before guest start", + "scope": {} + } + ], + "configSchema": { + "additionalProperties": false, + "type": "object" + }, + "provides": [ + { + "id": "github.com/wago-org/net/service", + "major": 1 + } + ] + }, + "definitionDigest": "sha256:05a8005ed12acbeeef96ccc5b16d13589eef259d48acef06eb2acc04292d08aa" + }, + { + "importPath": "github.com/wago-org/net/register", + "definition": { + "id": "github.com/wago-org/net/ipv6", + "name": "Wago IPv6", + "version": "0.1.0", + "description": "Bounded checked IPv6 networking.", + "stability": "experimental", + "compatibility": { + "engines": { + "wago": "\u003e=0.1.0" + } + }, + "provenance": { + "homepage": "https://github.com/wago-org/net#readme", + "repository": "https://github.com/wago-org/net", + "license": "Apache-2.0", + "authors": [ + "Wago contributors" + ] + }, + "authorities": [ + { + "name": "host.caller.identify", + "mode": "required", + "reason": "resolve exact synchronous callers without instance authority", + "scope": {} + }, + { + "name": "host.import.define", + "mode": "required", + "reason": "define the selected checked networking guest imports", + "scope": { + "modules": [ + "wago_net", + "wago_net_ipv6" + ] + } + }, + { + "name": "instance.close.observe", + "mode": "required", + "reason": "detach network state before exact instance resources close", + "scope": {} + }, + { + "name": "instance.instantiate.intercept", + "mode": "required", + "reason": "transactionally attach isolated network state before guest start", + "scope": {} + } + ], + "configSchema": { + "additionalProperties": false, + "type": "object" + }, + "provides": [ + { + "id": "github.com/wago-org/net/service", + "major": 1 + } + ] + }, + "definitionDigest": "sha256:3a2f779b359461b0d7110a8dc7df91c940e3bbf5bd7127b64f0ad379a7ec0970" + }, + { + "importPath": "github.com/wago-org/net/register", + "definition": { + "id": "github.com/wago-org/net/linklocal4", + "name": "Wago IPv4 Link-Local", + "version": "0.1.0", + "description": "Bounded checked IPv4 link-local networking.", + "stability": "experimental", + "compatibility": { + "engines": { + "wago": "\u003e=0.1.0" + } + }, + "provenance": { + "homepage": "https://github.com/wago-org/net#readme", + "repository": "https://github.com/wago-org/net", + "license": "Apache-2.0", + "authors": [ + "Wago contributors" + ] + }, + "authorities": [ + { + "name": "host.caller.identify", + "mode": "required", + "reason": "resolve exact synchronous callers without instance authority", + "scope": {} + }, + { + "name": "host.import.define", + "mode": "required", + "reason": "define the selected checked networking guest imports", + "scope": { + "modules": [ + "wago_net", + "wago_net_linklocal4" + ] + } + }, + { + "name": "instance.close.observe", + "mode": "required", + "reason": "detach network state before exact instance resources close", + "scope": {} + }, + { + "name": "instance.instantiate.intercept", + "mode": "required", + "reason": "transactionally attach isolated network state before guest start", + "scope": {} + } + ], + "configSchema": { + "additionalProperties": false, + "type": "object" + }, + "provides": [ + { + "id": "github.com/wago-org/net/service", + "major": 1 + } + ] + }, + "definitionDigest": "sha256:d3ea865c0f29eeb27c6791b5fd8537cdbf802308f6d64df70ce7b8d0c0d24647" + }, + { + "importPath": "github.com/wago-org/net/register", + "definition": { + "id": "github.com/wago-org/net/mdns", + "name": "Wago mDNS", + "version": "0.1.0", + "description": "Bounded checked multicast DNS networking.", + "stability": "experimental", + "compatibility": { + "engines": { + "wago": "\u003e=0.1.0" + } + }, + "provenance": { + "homepage": "https://github.com/wago-org/net#readme", + "repository": "https://github.com/wago-org/net", + "license": "Apache-2.0", + "authors": [ + "Wago contributors" + ] + }, + "authorities": [ + { + "name": "host.caller.identify", + "mode": "required", + "reason": "resolve exact synchronous callers without instance authority", + "scope": {} + }, + { + "name": "host.import.define", + "mode": "required", + "reason": "define the selected checked networking guest imports", + "scope": { + "modules": [ + "wago_net", + "wago_net_mdns" + ] + } + }, + { + "name": "instance.close.observe", + "mode": "required", + "reason": "detach network state before exact instance resources close", + "scope": {} + }, + { + "name": "instance.instantiate.intercept", + "mode": "required", + "reason": "transactionally attach isolated network state before guest start", + "scope": {} + } + ], + "configSchema": { + "additionalProperties": false, + "type": "object" + }, + "provides": [ + { + "id": "github.com/wago-org/net/service", + "major": 1 + } + ] + }, + "definitionDigest": "sha256:fd86c6b6e6ae88b93d931caea3085f3c62757267333e0202cfefbef68c6dd457" + }, + { + "importPath": "github.com/wago-org/net/register", + "definition": { + "id": "github.com/wago-org/net/ntp", + "name": "Wago NTP", + "version": "0.1.0", + "description": "Bounded checked NTP networking.", + "stability": "experimental", + "compatibility": { + "engines": { + "wago": "\u003e=0.1.0" + } + }, + "provenance": { + "homepage": "https://github.com/wago-org/net#readme", + "repository": "https://github.com/wago-org/net", + "license": "Apache-2.0", + "authors": [ + "Wago contributors" + ] + }, + "authorities": [ + { + "name": "host.caller.identify", + "mode": "required", + "reason": "resolve exact synchronous callers without instance authority", + "scope": {} + }, + { + "name": "host.import.define", + "mode": "required", + "reason": "define the selected checked networking guest imports", + "scope": { + "modules": [ + "wago_net", + "wago_net_ntp" + ] + } + }, + { + "name": "instance.close.observe", + "mode": "required", + "reason": "detach network state before exact instance resources close", + "scope": {} + }, + { + "name": "instance.instantiate.intercept", + "mode": "required", + "reason": "transactionally attach isolated network state before guest start", + "scope": {} + } + ], + "configSchema": { + "additionalProperties": false, + "type": "object" + }, + "provides": [ + { + "id": "github.com/wago-org/net/service", + "major": 1 + } + ] + }, + "definitionDigest": "sha256:da3ce096e5c87d1a79f783b9250034dfa3d79ac4bd3206cc9ac1028ecd335da7" + }, + { + "importPath": "github.com/wago-org/net/register", + "definition": { + "id": "github.com/wago-org/net/tcp", + "name": "Wago TCP", + "version": "0.1.0", + "description": "Bounded checked TCP networking.", + "stability": "experimental", + "compatibility": { + "engines": { + "wago": "\u003e=0.1.0" + } + }, + "provenance": { + "homepage": "https://github.com/wago-org/net#readme", + "repository": "https://github.com/wago-org/net", + "license": "Apache-2.0", + "authors": [ + "Wago contributors" + ] + }, + "authorities": [ + { + "name": "host.caller.identify", + "mode": "required", + "reason": "resolve exact synchronous callers without instance authority", + "scope": {} + }, + { + "name": "host.import.define", + "mode": "required", + "reason": "define the selected checked networking guest imports", + "scope": { + "modules": [ + "wago_net", + "wago_net_tcp" + ] + } + }, + { + "name": "instance.close.observe", + "mode": "required", + "reason": "detach network state before exact instance resources close", + "scope": {} + }, + { + "name": "instance.instantiate.intercept", + "mode": "required", + "reason": "transactionally attach isolated network state before guest start", + "scope": {} + } + ], + "configSchema": { + "additionalProperties": false, + "type": "object" + }, + "provides": [ + { + "id": "github.com/wago-org/net/service", + "major": 1 + } + ] + }, + "definitionDigest": "sha256:d3983477b6e75fc58cdc7df4330a27b7f83a119dd11cb557c17f04c3b7d905c0" + }, + { + "importPath": "github.com/wago-org/net/register", + "definition": { + "id": "github.com/wago-org/net/udp", + "name": "Wago UDP", + "version": "0.1.0", + "description": "Bounded checked UDP networking.", + "stability": "experimental", + "compatibility": { + "engines": { + "wago": "\u003e=0.1.0" + } + }, + "provenance": { + "homepage": "https://github.com/wago-org/net#readme", + "repository": "https://github.com/wago-org/net", + "license": "Apache-2.0", + "authors": [ + "Wago contributors" + ] + }, + "authorities": [ + { + "name": "host.caller.identify", + "mode": "required", + "reason": "resolve exact synchronous callers without instance authority", + "scope": {} + }, + { + "name": "host.import.define", + "mode": "required", + "reason": "define the selected checked networking guest imports", + "scope": { + "modules": [ + "wago_net", + "wago_net_udp" + ] + } + }, + { + "name": "instance.close.observe", + "mode": "required", + "reason": "detach network state before exact instance resources close", + "scope": {} + }, + { + "name": "instance.instantiate.intercept", + "mode": "required", + "reason": "transactionally attach isolated network state before guest start", + "scope": {} + } + ], + "configSchema": { + "additionalProperties": false, + "type": "object" + }, + "provides": [ + { + "id": "github.com/wago-org/net/service", + "major": 1 + } + ] + }, + "definitionDigest": "sha256:945a182caf9b06b4b366e009c15639bc1442bedea11176e2a50ebf8e51241c2f" + } + ] +} diff --git a/worker_lifecycle_test.go b/worker_lifecycle_test.go deleted file mode 100644 index 057e4ef..0000000 --- a/worker_lifecycle_test.go +++ /dev/null @@ -1,488 +0,0 @@ -//go:build !tinygo - -package net - -import ( - "context" - "errors" - "fmt" - "net/netip" - goruntime "runtime" - "strings" - "sync" - "testing" - "time" - - instancestate "github.com/wago-org/net/internal/instance/core" - dnsinstance "github.com/wago-org/net/internal/instance/dns" - tcpinstance "github.com/wago-org/net/internal/instance/tcp" - udpinstance "github.com/wago-org/net/internal/instance/udp" - "github.com/wago-org/net/internal/namespace" - "github.com/wago-org/net/internal/quota" - "github.com/wago-org/net/internal/resource" - wago "github.com/wago-org/wago" - "github.com/wago-org/wago/src/core/compiler/wasm" - "github.com/wago-org/wago/testutil/wasmtest" -) - -type workerNetworkSnapshot struct { - instance *wago.Instance - state *instancestate.State - udp resource.Handle - tcp resource.Handle - dns resource.Handle -} - -type workerHostSnapshot struct { - instance *wago.Instance - state *instancestate.State -} - -type workerNetworkExtension struct { - network *Extension - workers *wago.Workers - - created chan workerNetworkSnapshot - callers chan workerHostSnapshot - exits chan wago.WorkerExitContext - spawnErr chan error - hookErr chan error - - mu sync.Mutex - events []string -} - -func newWorkerNetworkExtension(network *Extension) *workerNetworkExtension { - return &workerNetworkExtension{ - network: network, - created: make(chan workerNetworkSnapshot, 8), - callers: make(chan workerHostSnapshot, 8), - exits: make(chan wago.WorkerExitContext, 8), - spawnErr: make(chan error, 8), - hookErr: make(chan error, 8), - } -} - -func (*workerNetworkExtension) Info() wago.ExtensionInfo { - return wago.ExtensionInfo{ID: "test.net-worker-lifecycle", Version: "1.0.0", Stability: wago.Stable} -} - -func (e *workerNetworkExtension) Register(reg *wago.Registry) error { - e.workers = reg.Workers() - e.workers.OnExit(func(ctx *wago.WorkerExitContext) { - if ctx != nil { - e.exits <- *ctx - } - }) - reg.Hooks().AfterInstantiate(func(ctx *wago.InstantiateContext, instance *wago.Instance) error { - if ctx.Origin != wago.InstantiateWorker { - return nil - } - state, ok := e.network.instanceManager().ForInstance(instance) - if !ok { - return errors.New("worker networking state was not attached first") - } - udp, progress, err := udpinstance.Bind(state, state.NamespaceHandle(), namespace.Endpoint{Address: netip.MustParseAddr("192.0.2.81"), Port: 4381}) - if err != nil || progress != namespace.ProgressDone { - return fmt.Errorf("bind worker UDP: handle=%v progress=%v: %w", udp, progress, err) - } - tcp, progress, err := tcpinstance.Listen(state, state.NamespaceHandle(), namespace.Endpoint{Address: netip.MustParseAddr("192.0.2.81"), Port: 4481}) - if err != nil || progress != namespace.ProgressDone { - return fmt.Errorf("listen worker TCP: handle=%v progress=%v: %w", tcp, progress, err) - } - dns, progress, err := dnsinstance.Resolve(state, state.NamespaceHandle(), namespace.DNSRequest{Name: "example.com", Types: namespace.DNSRecordsA}) - if err != nil || progress != namespace.ProgressInProgress { - return fmt.Errorf("resolve worker DNS: handle=%v progress=%v: %w", dns, progress, err) - } - e.created <- workerNetworkSnapshot{instance: instance, state: state, udp: udp, tcp: tcp, dns: dns} - return nil - }) - reg.Hooks().BeforeClose(func(ctx *wago.InstanceContext) { - if ctx.Origin != wago.InstantiateWorker { - return - } - e.addEvent("observe") - if state, ok := e.network.instanceManager().ForInstance(ctx.Instance); !ok || state == nil { - e.recordHookError(errors.New("worker networking state detached before reverse close observer")) - } - }) - reg.Hooks().BeforeClose(func(ctx *wago.InstanceContext) { - if ctx.Origin == wago.InstantiateWorker { - e.addEvent("panic") - panic("worker close observer panic") - } - }) - reg.Hooks().AfterClose(func(ctx *wago.InstanceContext) { - if ctx.Origin != wago.InstantiateWorker { - return - } - e.addEvent("after") - if _, ok := e.network.instanceManager().ForInstance(ctx.Instance); ok { - e.recordHookError(errors.New("worker networking state survived AfterClose")) - } - }) - - module := reg.ImportModule("worker_net") - module.Func("spawn", func(caller wago.HostModule, _, results []uint64) { - id, err := e.workers.Spawn(caller, 0, wago.WorkerOptions{QueueCapacity: 1, MaxPayloadBytes: 1, MaxQueueBytes: 1}) - if err == nil { - err = e.workers.Link(caller, id) - if err != nil { - _ = e.workers.Kill(id) - } - } - if err != nil { - e.spawnErr <- fmt.Errorf("caller %T: %w", caller, err) - return - } - results[0] = uint64(id) - }).Results(wago.ValI64) - module.Func("wait", func(caller wago.HostModule, _, _ []uint64) { - identity, ok := caller.(wago.InstanceHostModule) - if !ok || identity.Instance() == nil { - e.recordHookError(errors.New("worker host call lacked exact instance identity")) - } else { - state, found := e.network.instanceManager().FromHost(caller) - if !found { - e.recordHookError(errors.New("worker host identity did not resolve networking state")) - } - e.callers <- workerHostSnapshot{instance: identity.Instance(), state: state} - } - if err := e.workers.DispatchNext(caller); err != nil { - e.recordHookError(fmt.Errorf("worker dispatch returned: %w", err)) - } - }) - return nil -} - -func (e *workerNetworkExtension) addEvent(event string) { - e.mu.Lock() - e.events = append(e.events, event) - e.mu.Unlock() -} - -func (e *workerNetworkExtension) recordedEvents() []string { - e.mu.Lock() - defer e.mu.Unlock() - return append([]string(nil), e.events...) -} - -func (e *workerNetworkExtension) recordHookError(err error) { - select { - case e.hookErr <- err: - default: - } -} - -func workerNetworkingConfig() Config { - limits := QuotaLimits{ - Resources: 4, UDPResources: 1, TCPResources: 1, DNSResources: 1, - QueuedBytes: 8192, DNSWork: 1, ServiceUnits: 128, - } - ready := ReadinessConfig{MaxRegistrations: 4} - return Config{ - Policy: PolicyConfig{Rules: []PolicyRule{ - { - Action: PolicyAllow, Transports: []PolicyTransport{PolicyTransportUDP, PolicyTransportTCP}, - Directions: []PolicyDirection{PolicyInbound, PolicyOutbound}, Prefixes: []netip.Prefix{netip.MustParsePrefix("192.0.2.0/24")}, - }, - { - Action: PolicyAllow, Transports: []PolicyTransport{PolicyTransportDNS}, - Directions: []PolicyDirection{PolicyOutbound}, DNSSuffixes: []string{"example.com"}, - }, - }}, - Limits: &limits, Readiness: &ready, - StaticIPv4: &StaticIPv4Config{ - Hostname: "worker-net", RandSeed: 81, - HardwareAddress: [6]byte{2, 0, 0, 0, 0, 81}, GatewayHardwareAddress: [6]byte{2, 0, 0, 0, 0, 82}, - IPv4Address: netip.MustParseAddr("192.0.2.81"), MTU: 1500, - Link: PacketLinkConfig{MaxFrameBytes: 1514, IngressFrames: 8, EgressFrames: 8}, - UDP: UDPConfig{MaxSockets: 1, ReceiveBytes: 64, TransmitBytes: 64, ReceiveDatagrams: 2, TransmitDatagrams: 2, MaxPayloadBytes: 32}, - TCP: TCPConfig{MaxListeners: 1, MaxOutboundStreams: 0, AcceptBacklog: 1, ReceiveBytes: 256, TransmitBytes: 256, TransmitPackets: 4}, - DNS: DNSConfig{Server: netip.MustParseAddr("192.0.2.53"), MaxQueries: 1, MaxRecords: 2, MaxResponseBytes: 512, MaxAttempts: 1, RetryServiceAttempts: 2}, - }, - } -} - -func workerNetworkingModule(t *testing.T, runtime *wago.Runtime, validCallback bool) *wago.Module { - t.Helper() - callbackType := uint32(1) - callbackParams := []wasm.ValType(nil) - if !validCallback { - callbackType = 2 - callbackParams = []wasm.ValType{wasm.I32} - } - spawnImport := append(append(wasmtest.Name("worker_net"), wasmtest.Name("spawn")...), 0x00, 0x00) - waitImport := append(append(wasmtest.Name("worker_net"), wasmtest.Name("wait")...), 0x00, 0x01) - entry := uint32(2) - module, err := runtime.Compile(wasmtest.Module( - wasmtest.Section(1, wasmtest.Vec( - wasmtest.FuncType(nil, []wasm.ValType{wasm.I64}), - wasmtest.FuncType(nil, nil), - wasmtest.FuncType(callbackParams, nil), - )), - wasmtest.Section(2, wasmtest.Vec(spawnImport, waitImport)), - wasmtest.Section(3, wasmtest.Vec(wasmtest.ULEB(callbackType), wasmtest.ULEB(0))), - wasmtest.Section(4, wasmtest.Vec([]byte{0x70, 0x00, 0x01})), - wasmtest.Section(7, wasmtest.Vec(wasmtest.ExportEntry("run", 0, 3))), - wasmtest.Section(9, wasmtest.Vec(append([]byte{0x00, 0x41, 0x00, 0x0b, 0x01}, wasmtest.ULEB(entry)...))), - wasmtest.Section(10, wasmtest.Vec( - wasmtest.Code([]byte{0x10, 0x01, 0x0b}), - wasmtest.Code([]byte{0x10, 0x00, 0x0b}), - )), - )) - if err != nil { - t.Fatalf("Compile worker networking module: %v", err) - } - return module -} - -func callWorkerSpawn(t *testing.T, instance *wago.Instance) wago.WorkerID { - t.Helper() - results, err := instance.Call(context.Background(), "run") - if err != nil { - t.Fatalf("Call worker spawn: %v", err) - } - if len(results) != 1 { - t.Fatalf("worker spawn results = %v", results) - } - return wago.WorkerID(results[0].I64()) -} - -func waitWorkerSnapshot(t *testing.T, channel <-chan workerNetworkSnapshot) workerNetworkSnapshot { - t.Helper() - select { - case snapshot := <-channel: - return snapshot - case <-time.After(5 * time.Second): - t.Fatal("timed out waiting for worker networking state") - return workerNetworkSnapshot{} - } -} - -func waitWorkerHost(t *testing.T, channel <-chan workerHostSnapshot) workerHostSnapshot { - t.Helper() - select { - case snapshot := <-channel: - return snapshot - case <-time.After(5 * time.Second): - t.Fatal("timed out waiting for worker host identity") - return workerHostSnapshot{} - } -} - -func waitWorkerExit(t *testing.T, channel <-chan wago.WorkerExitContext) wago.WorkerExitContext { - t.Helper() - select { - case exit := <-channel: - return exit - case <-time.After(5 * time.Second): - t.Fatal("timed out waiting for worker exit") - return wago.WorkerExitContext{} - } -} - -func assertWorkerNetworkingClosed(t *testing.T, snapshot workerNetworkSnapshot) { - t.Helper() - for _, owned := range []struct { - handle resource.Handle - kind resource.Kind - }{ - {snapshot.udp, resource.KindUDPSocket}, - {snapshot.tcp, resource.KindTCPListener}, - {snapshot.dns, resource.KindDNSQuery}, - } { - if _, err := snapshot.state.Resources().Lookup(owned.handle, owned.kind); !errors.Is(err, resource.ErrClosed) { - t.Fatalf("retired worker handle %v (%v) lookup = %v, want ErrClosed", owned.handle, owned.kind, err) - } - } - if usage, closed := snapshot.state.Quotas().Snapshot(); !closed || usage != (quota.Usage{}) { - t.Fatalf("worker quota after close = %+v, closed=%v", usage, closed) - } - if readiness := snapshot.state.Readiness().Snapshot(); !readiness.Closed || readiness.Registrations != 0 { - t.Fatalf("worker readiness after close = %+v", readiness) - } -} - -func assertNoWorkerHookError(t *testing.T, extension *workerNetworkExtension) { - t.Helper() - select { - case err := <-extension.hookErr: - t.Fatal(err) - default: - } -} - -func TestWorkerNetworkingReinstantiatesAndRetiresEveryProtocolBetweenClassLeases(t *testing.T) { - network := Init(workerNetworkingConfig()) - worker := newWorkerNetworkExtension(network) - runtime := wago.NewRuntime(wago.WithWorkerLimits(wago.WorkerLimits{MaxLiveWorkers: 1, MaxQueueBytes: 1})) - defer runtime.Close() - if err := runtime.Use(network); err != nil { - t.Fatalf("Use networking: %v", err) - } - if err := runtime.Use(worker); err != nil { - t.Fatalf("Use workers: %v", err) - } - class, err := runtime.Class(workerNetworkingModule(t, runtime, true), wago.ClassOptions{ - Pool: wago.PoolOptions{MinInstances: 1, MaxInstances: 1, Reset: wago.ResetMemorySnapshot}, - }) - if err != nil { - t.Fatalf("Class: %v", err) - } - if got := class.ResetPolicy(); got != wago.ResetReinstantiate { - t.Fatalf("worker networking class reset = %v, want reinstantiate", got) - } - - lease, err := class.Acquire(context.Background()) - if err != nil { - t.Fatalf("Acquire first lease: %v", err) - } - oldParent := lease.Instance() - oldParentState, ok := network.instanceManager().ForInstance(oldParent) - if !ok { - t.Fatal("first parent networking state missing") - } - firstID := callWorkerSpawn(t, oldParent) - if firstID == 0 { - select { - case spawnErr := <-worker.spawnErr: - t.Fatalf("first worker spawn: %v", spawnErr) - default: - t.Fatal("first worker ID is zero") - } - } - first := waitWorkerSnapshot(t, worker.created) - caller := waitWorkerHost(t, worker.callers) - if caller.instance != first.instance || caller.state != first.state { - t.Fatalf("worker host identity/state = %p/%p, attached = %p/%p", caller.instance, caller.state, first.instance, first.state) - } - if got := network.instanceManager().Len(); got != 2 { - t.Fatalf("states with first worker = %d, want 2", got) - } - if err := lease.Release(); err != nil { - t.Fatalf("Release first lease: %v", err) - } - firstExit := waitWorkerExit(t, worker.exits) - if firstExit.WorkerID != firstID || firstExit.Kind != wago.WorkerKilled || !errors.Is(firstExit.Err, wago.ErrWorkerParentClosed) { - t.Fatalf("first worker exit = %+v", firstExit) - } - assertWorkerNetworkingClosed(t, first) - if _, ok := network.instanceManager().ForInstance(first.instance); ok { - t.Fatal("first worker instance retained networking state") - } - if _, ok := network.instanceManager().ForInstance(oldParent); ok { - t.Fatal("first parent retained networking state") - } - if usage, closed := oldParentState.Quotas().Snapshot(); !closed || usage != (quota.Usage{}) { - t.Fatalf("first parent quota after release = %+v, closed=%v", usage, closed) - } - if got := network.instanceManager().Len(); got != 1 { - t.Fatalf("states after first replacement = %d, want one fresh parent", got) - } - if got := worker.recordedEvents(); fmt.Sprint(got) != fmt.Sprint([]string{"panic", "observe", "after"}) { - t.Fatalf("first worker close events = %v", got) - } - assertNoWorkerHookError(t, worker) - - freshLease, err := class.Acquire(context.Background()) - if err != nil { - t.Fatalf("Acquire fresh lease: %v", err) - } - freshParent := freshLease.Instance() - freshParentState, ok := network.instanceManager().ForInstance(freshParent) - if !ok || freshParent == oldParent || freshParentState == oldParentState { - t.Fatalf("fresh parent/state = %p/%p, old = %p/%p, attached=%v", freshParent, freshParentState, oldParent, oldParentState, ok) - } - secondID := callWorkerSpawn(t, freshParent) - if secondID <= firstID { - t.Fatalf("second worker ID = %d, want greater than %d", secondID, firstID) - } - second := waitWorkerSnapshot(t, worker.created) - secondCaller := waitWorkerHost(t, worker.callers) - if second.instance == first.instance || second.state == first.state || secondCaller.instance != second.instance || secondCaller.state != second.state { - t.Fatalf("second worker identity/state was not fresh") - } - if err := freshLease.Release(); err != nil { - t.Fatalf("Release fresh lease: %v", err) - } - secondExit := waitWorkerExit(t, worker.exits) - if secondExit.WorkerID != secondID || secondExit.Kind != wago.WorkerKilled || !errors.Is(secondExit.Err, wago.ErrWorkerParentClosed) { - t.Fatalf("second worker exit = %+v", secondExit) - } - assertWorkerNetworkingClosed(t, second) - assertNoWorkerHookError(t, worker) - if err := class.Close(); err != nil { - t.Fatalf("Class.Close: %v", err) - } - if got := network.instanceManager().Len(); got != 0 { - t.Fatalf("states after class close = %d, want 0", got) - } -} - -func TestFailedWorkerCallbackValidationRetiresNetworkingAndReleasesWorkerQuota(t *testing.T) { - network := Init(workerNetworkingConfig()) - worker := newWorkerNetworkExtension(network) - runtime := wago.NewRuntime(wago.WithWorkerLimits(wago.WorkerLimits{MaxLiveWorkers: 1, MaxQueueBytes: 1})) - defer runtime.Close() - if err := runtime.Use(network); err != nil { - t.Fatalf("Use networking: %v", err) - } - if err := runtime.Use(worker); err != nil { - t.Fatalf("Use workers: %v", err) - } - invalidModule := workerNetworkingModule(t, runtime, false) - invalid, err := runtime.Instantiate(context.Background(), invalidModule) - if err != nil { - t.Fatalf("Instantiate invalid-callback parent: %v", err) - } - if id := callWorkerSpawn(t, invalid); id != 0 { - t.Fatalf("invalid callback worker ID = %d, want 0", id) - } - select { - case spawnErr := <-worker.spawnErr: - if spawnErr == nil { - t.Fatal("invalid callback spawn error is nil") - } - if !strings.Contains(spawnErr.Error(), "signature") { - t.Fatalf("invalid callback spawn error = %v", spawnErr) - } - case <-time.After(5 * time.Second): - t.Fatal("timed out waiting for invalid callback spawn error") - } - failed := waitWorkerSnapshot(t, worker.created) - assertWorkerNetworkingClosed(t, failed) - if got := network.instanceManager().Len(); got != 1 { - t.Fatalf("states after failed worker spawn = %d, want parent only", got) - } - assertNoWorkerHookError(t, worker) - if err := invalid.Close(); err != nil { - t.Fatalf("Close invalid parent: %v", err) - } - goruntime.KeepAlive(invalidModule) - - validModule := workerNetworkingModule(t, runtime, true) - valid, err := runtime.Instantiate(context.Background(), validModule) - if err != nil { - t.Fatalf("Instantiate valid parent: %v", err) - } - id := callWorkerSpawn(t, valid) - if id == 0 { - t.Fatal("worker quota was not released after failed spawn") - } - live := waitWorkerSnapshot(t, worker.created) - _ = waitWorkerHost(t, worker.callers) - if err := valid.Close(); err != nil { - t.Fatalf("Close valid parent: %v", err) - } - exit := waitWorkerExit(t, worker.exits) - if exit.WorkerID != id || !errors.Is(exit.Err, wago.ErrWorkerParentClosed) { - t.Fatalf("valid worker exit = %+v", exit) - } - assertWorkerNetworkingClosed(t, live) - goruntime.KeepAlive(validModule) - assertNoWorkerHookError(t, worker) - if got := network.instanceManager().Len(); got != 0 { - t.Fatalf("states after validation cleanup = %d, want 0", got) - } -} From ee6b676a1dc761b2b1de768bc36ea17bb962c376 Mon Sep 17 00:00:00 2001 From: JairusSW Date: Wed, 12 Aug 2026 06:59:23 -0700 Subject: [PATCH 02/11] 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 3bac584..40cdcca 100644 --- a/go.mod +++ b/go.mod @@ -4,5 +4,5 @@ go 1.24 require ( github.com/soypat/lneto v0.0.0-20260710133615-ab1a0c735a8b - github.com/wago-org/wago v0.0.0-20260812133922-747a0520edb8 + github.com/wago-org/wago v0.0.0-20260812135637-485943d049a3 ) diff --git a/go.sum b/go.sum index ff50bda..43ab199 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,4 @@ github.com/soypat/lneto v0.0.0-20260710133615-ab1a0c735a8b h1:qcoYcwWsFVI7zu0SRqhGD+B07R8o+sLFHwZQ/LtsHkM= github.com/soypat/lneto v0.0.0-20260710133615-ab1a0c735a8b/go.mod h1:Be5PjwoYukvHFiUXxpYi8+ppH2F/gw/vjGBvFdv+Ti8= -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 176f920d6b581d43e972b83910feaeb592c709ee Mon Sep 17 00:00:00 2001 From: JairusSW Date: Wed, 12 Aug 2026 07:05:14 -0700 Subject: [PATCH 03/11] 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 40cdcca..2de7d67 100644 --- a/go.mod +++ b/go.mod @@ -4,5 +4,5 @@ go 1.24 require ( github.com/soypat/lneto v0.0.0-20260710133615-ab1a0c735a8b - github.com/wago-org/wago v0.0.0-20260812135637-485943d049a3 + github.com/wago-org/wago v0.0.0-20260812140322-f669c98a153e ) diff --git a/go.sum b/go.sum index 43ab199..9449925 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,4 @@ github.com/soypat/lneto v0.0.0-20260710133615-ab1a0c735a8b h1:qcoYcwWsFVI7zu0SRqhGD+B07R8o+sLFHwZQ/LtsHkM= github.com/soypat/lneto v0.0.0-20260710133615-ab1a0c735a8b/go.mod h1:Be5PjwoYukvHFiUXxpYi8+ppH2F/gw/vjGBvFdv+Ti8= -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 85e0ad7fe1ff5f58b0733cb48d23dfcf142b6e08 Mon Sep 17 00:00:00 2001 From: JairusSW Date: Wed, 12 Aug 2026 07:11:44 -0700 Subject: [PATCH 04/11] 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 2de7d67..9cd0dd6 100644 --- a/go.mod +++ b/go.mod @@ -4,5 +4,5 @@ go 1.24 require ( github.com/soypat/lneto v0.0.0-20260710133615-ab1a0c735a8b - github.com/wago-org/wago v0.0.0-20260812140322-f669c98a153e + github.com/wago-org/wago v0.0.0-20260812141033-492d7b4905ea ) diff --git a/go.sum b/go.sum index 9449925..795fe4b 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,4 @@ github.com/soypat/lneto v0.0.0-20260710133615-ab1a0c735a8b h1:qcoYcwWsFVI7zu0SRqhGD+B07R8o+sLFHwZQ/LtsHkM= github.com/soypat/lneto v0.0.0-20260710133615-ab1a0c735a8b/go.mod h1:Be5PjwoYukvHFiUXxpYi8+ppH2F/gw/vjGBvFdv+Ti8= -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 183b17eb37e69ec6edb76244ed586a7fb3e005d5 Mon Sep 17 00:00:00 2001 From: JairusSW Date: Wed, 12 Aug 2026 07:22:25 -0700 Subject: [PATCH 05/11] ci: test against pinned vNext runtime --- internal/dependencytest/ci_prepare_test.go | 2 +- scripts/ci-prepare-dependencies.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/dependencytest/ci_prepare_test.go b/internal/dependencytest/ci_prepare_test.go index d58e64c..d812024 100644 --- a/internal/dependencytest/ci_prepare_test.go +++ b/internal/dependencytest/ci_prepare_test.go @@ -20,7 +20,7 @@ func TestCIPrepareDependenciesSelectsExactWorktrees(t *testing.T) { t.Fatal(err) } for _, revision := range []string{ - "97e6f91e6c822491577faa86f3c30aa5a8fff1e8", + "492d7b4905eab44cba160bd7e1cc81dd86651f58", "ab1a0c735a8b534a1d6322a3e245bc11a09431e7", } { if !strings.Contains(string(scriptData), revision) { diff --git a/scripts/ci-prepare-dependencies.sh b/scripts/ci-prepare-dependencies.sh index eb040ee..209ee3b 100755 --- a/scripts/ci-prepare-dependencies.sh +++ b/scripts/ci-prepare-dependencies.sh @@ -2,7 +2,7 @@ set -euo pipefail root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) -readonly wago_revision=${CI_WAGO_REVISION:-97e6f91e6c822491577faa86f3c30aa5a8fff1e8} +readonly wago_revision=${CI_WAGO_REVISION:-492d7b4905eab44cba160bd7e1cc81dd86651f58} readonly lneto_revision=${CI_LNETO_REVISION:-ab1a0c735a8b534a1d6322a3e245bc11a09431e7} readonly wago_repository=${CI_WAGO_REPOSITORY:-https://github.com/wago-org/wago.git} readonly lneto_repository=${CI_LNETO_REPOSITORY:-https://github.com/soypat/lneto.git} From 77a3984c62fa17faad6313277586380e30c75e92 Mon Sep 17 00:00:00 2001 From: JairusSW Date: Wed, 12 Aug 2026 07:26:40 -0700 Subject: [PATCH 06/11] ci: align with cross-platform plugin runtime --- go.mod | 2 +- go.sum | 4 ++-- internal/dependencytest/ci_prepare_test.go | 2 +- scripts/ci-prepare-dependencies.sh | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/go.mod b/go.mod index 9cd0dd6..0d20356 100644 --- a/go.mod +++ b/go.mod @@ -4,5 +4,5 @@ go 1.24 require ( github.com/soypat/lneto v0.0.0-20260710133615-ab1a0c735a8b - github.com/wago-org/wago v0.0.0-20260812141033-492d7b4905ea + github.com/wago-org/wago v0.0.0-20260812142346-bdfc6aaafa12 ) diff --git a/go.sum b/go.sum index 795fe4b..9f8789e 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,4 @@ github.com/soypat/lneto v0.0.0-20260710133615-ab1a0c735a8b h1:qcoYcwWsFVI7zu0SRqhGD+B07R8o+sLFHwZQ/LtsHkM= github.com/soypat/lneto v0.0.0-20260710133615-ab1a0c735a8b/go.mod h1:Be5PjwoYukvHFiUXxpYi8+ppH2F/gw/vjGBvFdv+Ti8= -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= diff --git a/internal/dependencytest/ci_prepare_test.go b/internal/dependencytest/ci_prepare_test.go index d812024..87b02de 100644 --- a/internal/dependencytest/ci_prepare_test.go +++ b/internal/dependencytest/ci_prepare_test.go @@ -20,7 +20,7 @@ func TestCIPrepareDependenciesSelectsExactWorktrees(t *testing.T) { t.Fatal(err) } for _, revision := range []string{ - "492d7b4905eab44cba160bd7e1cc81dd86651f58", + "bdfc6aaafa122816cc4147c737d25f7faa8c0a76", "ab1a0c735a8b534a1d6322a3e245bc11a09431e7", } { if !strings.Contains(string(scriptData), revision) { diff --git a/scripts/ci-prepare-dependencies.sh b/scripts/ci-prepare-dependencies.sh index 209ee3b..baf1621 100755 --- a/scripts/ci-prepare-dependencies.sh +++ b/scripts/ci-prepare-dependencies.sh @@ -2,7 +2,7 @@ set -euo pipefail root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) -readonly wago_revision=${CI_WAGO_REVISION:-492d7b4905eab44cba160bd7e1cc81dd86651f58} +readonly wago_revision=${CI_WAGO_REVISION:-bdfc6aaafa122816cc4147c737d25f7faa8c0a76} readonly lneto_revision=${CI_LNETO_REVISION:-ab1a0c735a8b534a1d6322a3e245bc11a09431e7} readonly wago_repository=${CI_WAGO_REPOSITORY:-https://github.com/wago-org/wago.git} readonly lneto_repository=${CI_LNETO_REPOSITORY:-https://github.com/soypat/lneto.git} From cf5f48d4214aa2e03a84d445f2f16788857bf9aa Mon Sep 17 00:00:00 2001 From: JairusSW Date: Wed, 12 Aug 2026 07:29:54 -0700 Subject: [PATCH 07/11] test: follow full plugin IDs in provenance fixtures --- internal/releaseprovenance/provenance_test.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/internal/releaseprovenance/provenance_test.go b/internal/releaseprovenance/provenance_test.go index fd6690f..b526f37 100644 --- a/internal/releaseprovenance/provenance_test.go +++ b/internal/releaseprovenance/provenance_test.go @@ -9,6 +9,8 @@ import ( "sort" "strings" "testing" + + "github.com/wago-org/net/internal/inspectionpolicy" ) func TestScanArtifactsIsSortedChecksummedAndExcludesProvenance(t *testing.T) { @@ -80,15 +82,15 @@ func TestInspectionEvidenceRejectsStaleAggregateAndRequiresEveryBundle(t *testin } stale := `{"capabilities":["net.dns","net.info","net.tcp","net.udp"],"imports":[{"module":"wago_net"},{"module":"wago_net_dns"},{"module":"wago_net_tcp"},{"module":"wago_net_udp"}]}` - writeTestFile(t, filepath.Join(dir, "custom-cli", "inspection-net-go.json"), stale) - writeTestFile(t, filepath.Join(dir, "custom-cli", "inspection-net-tinygo.json"), stale) + writeTestFile(t, filepath.Join(dir, "custom-cli", "inspection-"+inspectionpolicy.AggregateKey+"-go.json"), stale) + writeTestFile(t, filepath.Join(dir, "custom-cli", "inspection-"+inspectionpolicy.AggregateKey+"-tinygo.json"), stale) if err := readInspection(dir, new(Inspection)); err == nil { t.Fatal("stale four-capability aggregate unexpectedly accepted") } dir = t.TempDir() writeCanonicalInspectionEvidence(t, dir) - if err := os.Remove(filepath.Join(dir, "custom-cli", "inspection-net-udp-tinygo.json")); err != nil { + if err := os.Remove(filepath.Join(dir, "custom-cli", "inspection-"+inspectionpolicy.AggregateKey+"/udp-tinygo.json")); err != nil { t.Fatal(err) } if err := readInspection(dir, new(Inspection)); err == nil { From 98fe9793fbf96f349f333411e8aa0766a6154019 Mon Sep 17 00:00:00 2001 From: JairusSW Date: Wed, 12 Aug 2026 07:33:49 -0700 Subject: [PATCH 08/11] ci: track current Wago 386 blockers --- scripts/ci-386.sh | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/scripts/ci-386.sh b/scripts/ci-386.sh index 81eab16..6957ddd 100755 --- a/scripts/ci-386.sh +++ b/scripts/ci-386.sh @@ -65,9 +65,11 @@ if GOOS=linux GOARCH=386 CGO_ENABLED=0 go test ./... >"$log" 2>&1; then fi cat "$log" -readonly blocker_path='.audit/wago/src/core/compiler/frontend/frontend.go:' -readonly blocker_message='undefined: runtime.HostCtrlFrameBytes' -if ! grep -Fq "$blocker_path" "$log" || ! grep -Fq "$blocker_message" "$log"; then +readonly plugin_blocker_path='.audit/wago/src/core/plugins/' +readonly runtime_blocker_path='.audit/wago/src/core/runtime/' +readonly blocker_pattern='undefined: (cloneMachineCode|prepareMachineCode|mmapCodeRW|SealCode|munmap|storeTrap|TrapInterrupted)$' +if ! grep -Fq "$plugin_blocker_path" "$log" || ! grep -Fq "$runtime_blocker_path" "$log" || + ! grep -Eq "$blocker_pattern" "$log"; then echo 'ci-386: full repository failed for a reason other than the recorded Wago blocker' >&2 exit 1 fi @@ -75,11 +77,11 @@ if grep -E -- '--- FAIL:|panic:' "$log" >/dev/null; then echo 'ci-386: tests failed in addition to the recorded Wago compile blocker' >&2 exit 1 fi -unexpected_diagnostics=$(grep -E '\.go:[0-9]+:[0-9]+:' "$log" | grep -Fv "$blocker_message" || true) +unexpected_diagnostics=$(grep -E '\.go:[0-9]+:[0-9]+:' "$log" | grep -Ev "$blocker_pattern" || true) if [[ -n "$unexpected_diagnostics" ]]; then printf 'ci-386: unexpected compiler diagnostics:\n%s\n' "$unexpected_diagnostics" >&2 exit 1 fi -echo 'ci-386: full build remains blocked only by pinned Wago runtime.HostCtrlFrameBytes support' >&2 +echo 'ci-386: full build remains blocked only by pinned Wago 386 JIT/runtime stubs' >&2 echo 'ci-386: backend-neutral 386 package tests passed; keep the full attempt visible until Wago supports 386' >&2 From 2201d0670946209dda9678e287dbb01d23c203cd Mon Sep 17 00:00:00 2001 From: JairusSW Date: Wed, 12 Aug 2026 07:41:15 -0700 Subject: [PATCH 09/11] ci: pin final plugin runtime --- go.mod | 2 +- go.sum | 4 ++-- internal/dependencytest/ci_prepare_test.go | 2 +- scripts/ci-prepare-dependencies.sh | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/go.mod b/go.mod index 0d20356..87640ab 100644 --- a/go.mod +++ b/go.mod @@ -4,5 +4,5 @@ go 1.24 require ( github.com/soypat/lneto v0.0.0-20260710133615-ab1a0c735a8b - github.com/wago-org/wago v0.0.0-20260812142346-bdfc6aaafa12 + github.com/wago-org/wago v0.0.0-20260812143921-6f5623623b4f ) diff --git a/go.sum b/go.sum index 9f8789e..70c714c 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,4 @@ github.com/soypat/lneto v0.0.0-20260710133615-ab1a0c735a8b h1:qcoYcwWsFVI7zu0SRqhGD+B07R8o+sLFHwZQ/LtsHkM= github.com/soypat/lneto v0.0.0-20260710133615-ab1a0c735a8b/go.mod h1:Be5PjwoYukvHFiUXxpYi8+ppH2F/gw/vjGBvFdv+Ti8= -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= diff --git a/internal/dependencytest/ci_prepare_test.go b/internal/dependencytest/ci_prepare_test.go index 87b02de..c5a69a4 100644 --- a/internal/dependencytest/ci_prepare_test.go +++ b/internal/dependencytest/ci_prepare_test.go @@ -20,7 +20,7 @@ func TestCIPrepareDependenciesSelectsExactWorktrees(t *testing.T) { t.Fatal(err) } for _, revision := range []string{ - "bdfc6aaafa122816cc4147c737d25f7faa8c0a76", + "6f5623623b4f54c7c9f69f3c06760168d345addf", "ab1a0c735a8b534a1d6322a3e245bc11a09431e7", } { if !strings.Contains(string(scriptData), revision) { diff --git a/scripts/ci-prepare-dependencies.sh b/scripts/ci-prepare-dependencies.sh index baf1621..594cea0 100755 --- a/scripts/ci-prepare-dependencies.sh +++ b/scripts/ci-prepare-dependencies.sh @@ -2,7 +2,7 @@ set -euo pipefail root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) -readonly wago_revision=${CI_WAGO_REVISION:-bdfc6aaafa122816cc4147c737d25f7faa8c0a76} +readonly wago_revision=${CI_WAGO_REVISION:-6f5623623b4f54c7c9f69f3c06760168d345addf} readonly lneto_revision=${CI_LNETO_REVISION:-ab1a0c735a8b534a1d6322a3e245bc11a09431e7} readonly wago_repository=${CI_WAGO_REPOSITORY:-https://github.com/wago-org/wago.git} readonly lneto_repository=${CI_LNETO_REPOSITORY:-https://github.com/soypat/lneto.git} From 530b91794a894f33e6aa1fd1e8aa5303259e40fb Mon Sep 17 00:00:00 2001 From: JairusSW Date: Wed, 12 Aug 2026 07:46:39 -0700 Subject: [PATCH 10/11] ci: pin verified plugin runtime --- go.mod | 2 +- go.sum | 4 ++-- internal/dependencytest/ci_prepare_test.go | 2 +- scripts/ci-prepare-dependencies.sh | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/go.mod b/go.mod index 87640ab..9a79c21 100644 --- a/go.mod +++ b/go.mod @@ -4,5 +4,5 @@ go 1.24 require ( github.com/soypat/lneto v0.0.0-20260710133615-ab1a0c735a8b - github.com/wago-org/wago v0.0.0-20260812143921-6f5623623b4f + github.com/wago-org/wago v0.0.0-20260812144524-1c58c9862d25 ) diff --git a/go.sum b/go.sum index 70c714c..10bcdb3 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,4 @@ github.com/soypat/lneto v0.0.0-20260710133615-ab1a0c735a8b h1:qcoYcwWsFVI7zu0SRqhGD+B07R8o+sLFHwZQ/LtsHkM= github.com/soypat/lneto v0.0.0-20260710133615-ab1a0c735a8b/go.mod h1:Be5PjwoYukvHFiUXxpYi8+ppH2F/gw/vjGBvFdv+Ti8= -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= diff --git a/internal/dependencytest/ci_prepare_test.go b/internal/dependencytest/ci_prepare_test.go index c5a69a4..bd3d3e8 100644 --- a/internal/dependencytest/ci_prepare_test.go +++ b/internal/dependencytest/ci_prepare_test.go @@ -20,7 +20,7 @@ func TestCIPrepareDependenciesSelectsExactWorktrees(t *testing.T) { t.Fatal(err) } for _, revision := range []string{ - "6f5623623b4f54c7c9f69f3c06760168d345addf", + "1c58c9862d25a0a564786284a1967831f1ae9d61", "ab1a0c735a8b534a1d6322a3e245bc11a09431e7", } { if !strings.Contains(string(scriptData), revision) { diff --git a/scripts/ci-prepare-dependencies.sh b/scripts/ci-prepare-dependencies.sh index 594cea0..8215eb9 100755 --- a/scripts/ci-prepare-dependencies.sh +++ b/scripts/ci-prepare-dependencies.sh @@ -2,7 +2,7 @@ set -euo pipefail root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) -readonly wago_revision=${CI_WAGO_REVISION:-6f5623623b4f54c7c9f69f3c06760168d345addf} +readonly wago_revision=${CI_WAGO_REVISION:-1c58c9862d25a0a564786284a1967831f1ae9d61} readonly lneto_revision=${CI_LNETO_REVISION:-ab1a0c735a8b534a1d6322a3e245bc11a09431e7} readonly wago_repository=${CI_WAGO_REPOSITORY:-https://github.com/wago-org/wago.git} readonly lneto_repository=${CI_LNETO_REPOSITORY:-https://github.com/soypat/lneto.git} From 20d8aa05a652ddfe1ee5623dd31e5f78ba231521 Mon Sep 17 00:00:00 2001 From: JairusSW Date: Wed, 12 Aug 2026 08:01:09 -0700 Subject: [PATCH 11/11] test(tls): service the peer handshake to completion --- internal/backend/gotls/stream_test.go | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/internal/backend/gotls/stream_test.go b/internal/backend/gotls/stream_test.go index c111407..be8ee0b 100644 --- a/internal/backend/gotls/stream_test.go +++ b/internal/backend/gotls/stream_test.go @@ -63,9 +63,26 @@ func TestClientHandshakeVerificationALPNAndPlaintext(t *testing.T) { t.Fatalf("handshake did not complete: ready=%v terminal=%v verified=%v client-out=%d server-out=%d", client.Readiness(), terminal, verified, client.bridge.cipherPending(), serverBridge.cipherPending()) } } - if err := <-serverDone; err != nil { - t.Fatal(err) + deadline := time.NewTimer(5 * time.Second) + defer deadline.Stop() + for { + select { + case err := <-serverDone: + if err != nil { + t.Fatal(err) + } + goto serverReady + case <-deadline.C: + t.Fatal("server handshake did not complete") + default: + if _, _, err := client.TryService(nscore.ServiceBudget{Packets: 8, Bytes: 64 << 10, Operations: 8}); err != nil { + t.Fatal(err) + } + runtime.Gosched() + } } + +serverReady: info, ok := client.ConnectionInfo() if !ok || info.NegotiatedALPN != "h2" || info.TLSVersion != cryptotls.VersionTLS13 || info.PeerLeafSPKI256 == ([32]byte{}) { t.Fatalf("connection info = %+v, %v", info, ok)