From d349826cb27b8e21203d395621ed9ca6bef2900d Mon Sep 17 00:00:00 2001 From: Robby Cochran Date: Thu, 3 Sep 2026 17:02:39 -0700 Subject: [PATCH] refactor: narrow OpenShell client capabilities --- internal/openshell/client.go | 24 ++++++++++++++++++++++++ internal/openshell/sdkclient/client.go | 3 +++ internal/plan/state.go | 4 ++-- internal/reconcile/inference.go | 2 +- internal/reconcile/provider.go | 2 +- 5 files changed, 31 insertions(+), 4 deletions(-) diff --git a/internal/openshell/client.go b/internal/openshell/client.go index c939f45..e1146df 100644 --- a/internal/openshell/client.go +++ b/internal/openshell/client.go @@ -74,6 +74,30 @@ type SandboxExecutionClient interface { DeleteSandbox(ctx context.Context, name string) error } +// StateReader reads the gateway state needed to build a workflow plan. +type StateReader interface { + Health(ctx context.Context) (Health, error) + Providers(ctx context.Context) ([]Provider, error) + InferenceRouteReader +} + +// InferenceRouteReader reads one inference route from the bound workspace. +type InferenceRouteReader interface { + GetInferenceRoute(ctx context.Context, route string) (InferenceRoute, error) +} + +// ProviderReconciler reads and updates non-secret provider configuration. +type ProviderReconciler interface { + GetProvider(ctx context.Context, name string) (Provider, error) + UpdateProvider(ctx context.Context, p Provider) (Provider, error) +} + +// InferenceReconciler reads and upserts inference routes. +type InferenceReconciler interface { + InferenceRouteReader + SetInferenceRoute(ctx context.Context, cfg InferenceRouteConfig) (InferenceRoute, error) +} + // InteractiveSession is the SDK-native bidirectional terminal stream without // exposing an SDK type outside sdkclient. type InteractiveSession interface { diff --git a/internal/openshell/sdkclient/client.go b/internal/openshell/sdkclient/client.go index 5fb0691..57e5530 100644 --- a/internal/openshell/sdkclient/client.go +++ b/internal/openshell/sdkclient/client.go @@ -26,6 +26,9 @@ var ( _ openshell.Factory = New _ openshell.Client = (*client)(nil) _ openshell.SandboxExecutionClient = (*client)(nil) + _ openshell.StateReader = (*client)(nil) + _ openshell.ProviderReconciler = (*client)(nil) + _ openshell.InferenceReconciler = (*client)(nil) ) // client wraps the SDK client interface, binding it to one workspace. It holds diff --git a/internal/plan/state.go b/internal/plan/state.go index df9f47b..f27b7c0 100644 --- a/internal/plan/state.go +++ b/internal/plan/state.go @@ -43,7 +43,7 @@ type CurrentState struct { // unreachable or unauthenticated, Reachable is set to false and a nil error is // returned; other errors are escalated. When desired configures inference, it // reads the current route so the plan can show a real create/update/noop diff. -func ReadCurrentState(ctx context.Context, c openshell.Client, desired *config.Harness) (CurrentState, error) { +func ReadCurrentState(ctx context.Context, c openshell.StateReader, desired *config.Harness) (CurrentState, error) { var state CurrentState // Read health. @@ -98,7 +98,7 @@ func ReadCurrentState(ctx context.Context, c openshell.Client, desired *config.H // back to a config-only validate. Transient errors (unavailable/unauthenticated) // and ErrPermission are propagated so the caller decides whether to degrade // (the read-only plan) or fail (the reconcile write path). -func ReadInferenceState(ctx context.Context, c openshell.Client, desired config.Inference) (InferenceState, error) { +func ReadInferenceState(ctx context.Context, c openshell.InferenceRouteReader, desired config.Inference) (InferenceState, error) { route, err := c.GetInferenceRoute(ctx, ResolveInferenceRoute(desired.Route)) switch { case err == nil: diff --git a/internal/reconcile/inference.go b/internal/reconcile/inference.go index 0b61212..4c1b5d4 100644 --- a/internal/reconcile/inference.go +++ b/internal/reconcile/inference.go @@ -43,7 +43,7 @@ type InferenceResult struct { // changes. Likewise, an update triggered by a provider/model change with an unset // timeout writes 0, resetting any non-default gateway timeout to the default — // "unset timeout" always means "let the gateway decide". -func ReconcileInference(ctx context.Context, c openshell.Client, desired config.Inference) (InferenceResult, error) { +func ReconcileInference(ctx context.Context, c openshell.InferenceReconciler, desired config.Inference) (InferenceResult, error) { cur, err := plan.ReadInferenceState(ctx, c, desired) if err != nil { return InferenceResult{}, fmt.Errorf("reading inference route: %w", err) diff --git a/internal/reconcile/provider.go b/internal/reconcile/provider.go index 532833d..3727799 100644 --- a/internal/reconcile/provider.go +++ b/internal/reconcile/provider.go @@ -45,7 +45,7 @@ type ProviderResult struct { // On Update the write is credential-preserving by construction (the firewall // Provider has no credentials field) and is reached only on a real non-secret // delta, so the empty-credential copy-through is never sent spuriously. -func ReconcileProviders(ctx context.Context, c openshell.Client, desired []config.Provider) ([]ProviderResult, error) { +func ReconcileProviders(ctx context.Context, c openshell.ProviderReconciler, desired []config.Provider) ([]ProviderResult, error) { results := make([]ProviderResult, 0, len(desired)) for _, d := range desired {