From ef28015c76bf9f2e17019646a529904d7950ddee Mon Sep 17 00:00:00 2001 From: Quick <31828688+Quick104@users.noreply.github.com> Date: Mon, 14 Sep 2026 19:47:48 +0000 Subject: [PATCH] fix(runtime): dial the host broker at bind time go-plugin keeps the connection info the host sends for a brokered stream for only five seconds (GRPCBroker.timeoutWait). The host sends it from AcceptAndServe just before calling BindHostBroker, but the SDK dialed the stream lazily on the first Host() call. A plugin whose first host call came later than that, which is the normal case for a resident network access provider idling until an admin connects it, found the stream expired and every Host() call returned nil for the rest of the process: Connect could not read host info or persist state and timed out. setBrokerID now dials as soon as the host binds the stream, while the window is open; runtimehost calls multiplex over that one connection as before. Observed against a running Silo stack: a provider connected within five seconds of start worked, one connected twenty seconds after start never could. Co-Authored-By: Claude Fable 5.1 --- examples/hello-network-access/manifest.json | 14 ++++++++ pkg/pluginsdk/runtime/runtime.go | 40 ++++++++++++++------- 2 files changed, 42 insertions(+), 12 deletions(-) diff --git a/examples/hello-network-access/manifest.json b/examples/hello-network-access/manifest.json index 0cf0301..2d44e1d 100644 --- a/examples/hello-network-access/manifest.json +++ b/examples/hello-network-access/manifest.json @@ -3,6 +3,20 @@ "version": "0.1.0", "checksum": "__CHECKSUM__", "silo_api_version": "v1", + "supported_platforms": [ + { + "os": "linux", + "arch": "amd64" + }, + { + "os": "linux", + "arch": "arm64" + }, + { + "os": "darwin", + "arch": "arm64" + } + ], "capabilities": [ { "type": "network_access_provider.v1", diff --git a/pkg/pluginsdk/runtime/runtime.go b/pkg/pluginsdk/runtime/runtime.go index 21ca160..475136c 100644 --- a/pkg/pluginsdk/runtime/runtime.go +++ b/pkg/pluginsdk/runtime/runtime.go @@ -262,28 +262,43 @@ func (s *pluginHostState) setBroker(b *plugin.GRPCBroker) { s.mu.Unlock() } +// setBrokerID records the host-assigned stream and dials it at once. +// +// The dial cannot wait for the first Host() call: go-plugin's broker keeps +// the connection info the host sent for a stream for only five seconds +// (GRPCBroker.timeoutWait), and the host sends it from its AcceptAndServe +// just before invoking BindHostBroker. A plugin whose first host call comes +// later than that, which is the normal case for a resident plugin that idles +// until an admin connects it, would find the stream expired and every +// Host() call would return nil for the life of the process. Dialing here +// pins the connection while the window is open; runtimehost calls then +// multiplex over it. func (s *pluginHostState) setBrokerID(id uint32) { s.mu.Lock() + defer s.mu.Unlock() s.brokerID = id // new stream id → drop any cached client s.client = nil - s.mu.Unlock() + s.dialLocked() } -func (s *pluginHostState) host() *runtimehost.Client { - s.mu.Lock() - defer s.mu.Unlock() - if s.client != nil { - return s.client - } - if s.broker == nil || s.brokerID == 0 { - return nil +// dialLocked connects to the bound stream if it has not been connected yet. +// The caller holds s.mu. +func (s *pluginHostState) dialLocked() { + if s.client != nil || s.broker == nil || s.brokerID == 0 { + return } conn, err := s.broker.Dial(s.brokerID) if err != nil { - return nil + return } s.client = runtimehost.NewClient(conn) +} + +func (s *pluginHostState) host() *runtimehost.Client { + s.mu.Lock() + defer s.mu.Unlock() + s.dialLocked() return s.client } @@ -295,8 +310,9 @@ func SetHostBrokerID(id uint32) { pluginHost.setBrokerID(id) } // Host returns a runtimehost.Client connected to the silo host. Returns // nil before the host has invoked Runtime.BindHostBroker (i.e. very briefly -// during plugin startup) or if the broker dial fails. Capability handlers -// should treat nil as transient and either skip or surface a temporary error. +// during plugin startup) or if the broker dial failed at bind time. +// Capability handlers should treat nil as transient and either skip or +// surface a temporary error. // // The first successful call dials the host broker stream and caches the // *runtimehost.Client; later calls reuse the same client.