Skip to content

feat: named interfaces#294

Draft
ricochet wants to merge 3 commits intomainfrom
name-interfaces
Draft

feat: named interfaces#294
ricochet wants to merge 3 commits intomainfrom
name-interfaces

Conversation

@ricochet
Copy link
Contributor

Named Host Interfaces: Multi-Backend Binding

A component may need multiple implementations of the same interface type (e.g., one wasi:keyvalue backed by NATS for caching and another backed by Redis for sessions). Previously, the WorkloadDeployment spec only supported one host interface entry per namespace:package combination -- EnsureHostInterface merged duplicate entries. This design adds a name field to HostInterface to allow multiple named entries of the same interface type.

Design

1. WorkloadDeployment YAML Schema

An optional name field on each hostInterface entry. When present, it allows multiple entries of the same namespace:package and serves as the identifier components pass to resource-opening functions (e.g., store::open("cache")).

apiVersion: runtime.wasmcloud.dev/v1alpha1
kind: WorkloadDeployment
metadata:
  name: my-app
spec:
  template:
    spec:
      components:
        - name: my-component
          image: ghcr.io/example/my-component:0.1.0
      hostInterfaces:
        # Named: NATS-backed keyvalue for caching
        - name: cache
          namespace: wasi
          package: keyvalue
          interfaces: [store, atomics, batch]
          config:
            backend: nats
            bucket: cache-kv
        # Named: Redis-backed keyvalue for sessions
        - name: sessions
          namespace: wasi
          package: keyvalue
          interfaces: [store]
          config:
            backend: redis
            url: redis://redis:6379
        # Unnamed: HTTP (backwards compatible, no name needed)
        - namespace: wasi
          package: http
          interfaces: [incoming-handler]
          config:
            host: my-app.localhost

Component code uses the name as the identifier in store::open:

let cache = wasi::keyvalue::store::open("cache")?;     // routes to NATS
let sessions = wasi::keyvalue::store::open("sessions")?; // routes to Redis

2. Naming Rules

  • name is optional. When absent, behavior is identical to before (backwards compatible).
  • When multiple entries share the same namespace:package, all of them must have a non-empty name.
  • Names must be unique within a workload's hostInterfaces of the same namespace:package.
  • Names are strings matching [a-z0-9][a-z0-9-]* (DNS label style).
  • The name directly maps to the identifier parameter in resource-opening functions (store::open(name) for keyvalue, create-container(name) for blobstore, etc.).

3. Type Changes

3a. Go CRD (HostInterface)

File: runtime-operator/api/runtime/v1alpha1/workload_types.go

Added Name field with kubebuilder validation:

type HostInterface struct {
    ConfigLayer `json:",inline"`
    // +kubebuilder:validation:Optional
    // +kubebuilder:validation:Pattern=`^[a-z0-9][a-z0-9-]*$`
    Name string `json:"name,omitempty"`
    Namespace string `json:"namespace"`
    Package string `json:"package"`
    Interfaces []string `json:"interfaces,omitempty"`
    Version string `json:"version,omitempty"`
}

3b. EnsureHostInterface Merge Logic

File: runtime-operator/api/runtime/v1alpha1/workload_types.go

Updated to match on namespace+package+name (was just namespace+package):

if existing.Namespace == iface.Namespace &&
   existing.Package == iface.Package &&
   existing.Name == iface.Name {
    // merge...
}

3c. Protobuf (WitInterface)

File: proto/wasmcloud/runtime/v2/wit_interface.proto

Added field 6:

message WitInterface {
  string namespace = 1;
  string package = 2;
  string version = 3;
  repeated string interfaces = 4;
  map<string, string> config = 5;
  string name = 6;  // optional instance name for multi-backend routing
}

3d. Rust WitInterface

File: crates/wash-runtime/src/wit.rs

Added name: Option<String> field. Method changes:

Method Change
instance() Includes name when present: namespace:package/name@version
merge() Only merges if name also matches (via instance())
contains() No change -- name does not affect interface matching
Hash impl Includes name in hash
Display impl Shows name as namespace:package [name]/interfaces@version
From<&str> No change (name comes from spec, not parsing)

3e. Rust From Conversions

File: crates/wash-runtime/src/washlet/mod.rs

Both directions handle the new field:

  • From<types::v2::WitInterface>: maps empty string to None, non-empty to Some
  • From<crate::wit::WitInterface>: maps None to empty string, Some to the value

4. Runtime Multiplexing

The runtime side would work as follows (for future implementation):

  1. Single plugin per interface type: Only one wasi-keyvalue plugin registers with the linker. This is a wasmtime constraint (add_to_linker can only be called once per interface).

  2. Multiplexing plugin: A MultiplexKeyValue plugin wraps multiple backend implementations (NATS, Redis, in-memory, filesystem). It registers once and routes store::open(identifier) to the correct backend based on the name from the spec.

  3. bind_plugins unchanged: The existing matching logic in bind_plugins() already passes all matched WitInterface entries to the plugin via on_workload_bind. With named entries, the plugin receives multiple WitInterface objects (one per name), each with its own config. The plugin uses these to configure its routing table.

  4. Default fallback: When name is None, the plugin falls back to its default backend (equivalent to single-backend behavior).

5. Applicability to Other Interfaces

The name field is generic -- it works for any interface type. The routing mechanism is interface-specific:

Interface Routing Function name maps to
wasi:keyvalue store::open(identifier) identifier
wasi:blobstore create-container(name) name
wasmcloud:messaging subscription config config-based routing
wasmcloud:postgres connection identifier connection name

6. Backwards Compatibility

All changes are fully backwards compatible:

  • Protobuf: Adding field 6 is wire-compatible. Old clients don't send it (defaults to empty). Old servers ignore it.
  • Go CRD: Name is optional with omitempty. Existing manifests without name work identically.
  • Rust: name: Option<String> defaults to None. All existing matching/merging/binding logic is unchanged when name is absent.
  • EnsureHostInterface: When both entries have Name == "", the match condition is the same as before.

7. CRD Validation (Future)

Add a validation webhook or CEL rule:

  • If two or more hostInterfaces entries share the same namespace+package, all of them must have a non-empty, unique name.
  • No two entries can have the same namespace+package+name triple.

8. Files Modified

File Change
runtime-operator/api/runtime/v1alpha1/workload_types.go Added Name to HostInterface, updated EnsureHostInterface
runtime-operator/api/runtime/v1alpha1/workload_types_test.go New: unit tests for named host interfaces
proto/wasmcloud/runtime/v2/wit_interface.proto Added name field (field number 6)
crates/wash-runtime/src/wit.rs Added name: Option<String>, updated instance, merge, Hash, Display
crates/wash-runtime/src/washlet/mod.rs Updated both From<WitInterface> conversions
crates/wash-runtime/src/host/mod.rs Added name: None to struct constructions
crates/wash-runtime/src/engine/workload.rs Added name: None to test struct constructions
crates/wash-runtime/tests/integration_*.rs Added name: None to test struct constructions

9. Verification

  • Go unit tests: 4 tests covering separate entries with different names, merging with same name, unnamed backwards compatibility, and named vs unnamed distinction. All passing.
  • Rust unit tests: 7 new tests covering instance() with name, merge with matching/different/unnamed names, contains ignoring name, Hash including name, and Display with name. All 20 wit tests passing.
  • Workload engine tests: All 12 tests passing (existing tests unaffected).
  • Backwards compatibility: All existing struct constructions set name: None, preserving identical behavior.

@ricochet ricochet requested a review from a team as a code owner February 23, 2026 17:24
@github-actions
Copy link

github-actions bot commented Feb 23, 2026

The latest Buf updates on your PR. Results from workflow proto / lint (pull_request).

BuildFormatLintBreakingUpdated (UTC)
✅ passed✅ passed✅ passed✅ passedFeb 23, 2026, 8:23 PM

@ricochet ricochet marked this pull request as draft February 23, 2026 17:30
@ricochet
Copy link
Contributor Author

ricochet commented Feb 23, 2026

We need to ensure at deployment time that a plugin can fulfill the matched name statically.

Signed-off-by: Bailey Hayes <bailey@cosmonic.com>
Signed-off-by: Bailey Hayes <bailey@cosmonic.com>
@ricochet
Copy link
Contributor Author

Second commit covers...

Deployment-Time Validation

When a workload specifies multiple named entries of the same namespace:package, the matched plugin must explicitly declare support for named multiplexing. This prevents silent failures where a plugin receives multiple named entries but only handles one.

supports_named_instances() Trait Method

File: crates/wash-runtime/src/plugin/mod.rs

Added a default method to the HostPlugin trait:

fn supports_named_instances(&self) -> bool {
    false
}

The default false protects all existing plugins without code changes. Plugins that implement multiplexing override this to return true.

Validation in bind_plugins()

File: crates/wash-runtime/src/engine/workload.rs

After collecting plugin_matched_interfaces and before calling on_workload_bind(), the runtime checks:

  1. Groups matched interfaces by (namespace, package).
  2. For each group, collects entries that have a name.
  3. If more than one named entry exists for the same namespace:package and the plugin returns supports_named_instances() == false, binding fails with a clear error:
plugin 'keyvalue-plugin' does not support named instances, but workload
requires 2 named entries for wasi:keyvalue (names: cache, sessions).
The plugin must implement supports_named_instances() to handle multiplexed interfaces.

This runs before any side effects, so failed validation doesn't leave partial state.

Key behaviors:

  • Single named entry: No validation needed -- a plugin can handle one named entry without multiplexing support.
  • Unnamed entries: No validation -- backwards compatible with existing behavior.
  • Multiple named entries + supports_named_instances() == true: Passes validation. The plugin can still reject in on_workload_bind() for finer-grained checks.

@ricochet ricochet marked this pull request as ready for review February 23, 2026 18:47
@ricochet
Copy link
Contributor Author

Associating a name is similar to how we will on day in the Component Model have a feature called wit templates (currently scoped post-mvp): https://gist.github.com/lukewagner/e4bab34bbe73d4ce9cb6d8959bc69243

The name in this case has two uses, first as the identifier in the * import enumeration (it will be the world name in the future). We're additionally using that same identifier as the identifier for the connection.

Signed-off-by: Bailey Hayes <bailey@cosmonic.com>
@lxfontes
Copy link
Member

lxfontes commented Feb 23, 2026

suggestion: hold off so we can chat about this post bytecodealliance plumbers summit.
with the exception of messaging, those interfaces could be multi-backend today ( by implementing multiple backends at the plugin level )

@ricochet ricochet marked this pull request as draft February 24, 2026 15:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants