Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions internal/runtime/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package runtime

import (
"fmt"
"slices"
"strings"

"github.com/fullsend-ai/fullsend/internal/config"
Expand Down Expand Up @@ -29,19 +30,43 @@ func Resolve(name string) (Backend, error) {
}

// ResolveFromConfig selects the runtime backend from org config defaults.
// The runtime name is validated against [config.ValidRuntimes] before
// resolution so that stub runtimes registered in [Resolve] for dev/testing
// cannot be activated through config files.
func ResolveFromConfig(cfg config.OrgConfigReader) (Backend, error) {
rt := "claude"
if cfg != nil && cfg.OrgRepoDefaults().Runtime != "" {
rt = cfg.OrgRepoDefaults().Runtime
}
if err := validateConfigRuntime(rt); err != nil {
return Backend{}, err
}
return Resolve(rt)
}

// ResolveFromPerRepoConfig selects the runtime backend from per-repo config.
// The runtime name is validated against [config.ValidRuntimes] before
// resolution so that stub runtimes registered in [Resolve] for dev/testing
// cannot be activated through config files.
func ResolveFromPerRepoConfig(cfg config.PerRepoConfigReader) (Backend, error) {
rt := "claude"
if cfg != nil && cfg.ConfigRuntime() != "" {
rt = cfg.ConfigRuntime()
}
if err := validateConfigRuntime(rt); err != nil {
return Backend{}, err
}
return Resolve(rt)
}

// validateConfigRuntime checks that rt is in the set of user-facing
// runtimes allowed in config files. Stub runtimes (e.g. "opencode")
// are intentionally excluded from [config.ValidRuntimes] so they
// cannot be activated through org or per-repo config.
func validateConfigRuntime(rt string) error {
valid := config.ValidRuntimes()
if !slices.Contains(valid, rt) {
return fmt.Errorf("invalid runtime %q in config: must be one of %s", rt, strings.Join(valid, ", "))
}
return nil
}
49 changes: 41 additions & 8 deletions internal/runtime/registry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,14 +69,6 @@ func TestResolveFromPerRepoConfig(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "dummy", dummyBackend.Runtime.Name())

// opencode is not in ValidRuntimes() but is resolvable via Resolve().
// A hand-written config bypassing validation can reach the stub.
ocCfg := config.NewPerRepoConfig(nil, "")
ocCfg.SetRuntime("opencode")
ocBackend, err := ResolveFromPerRepoConfig(ocCfg)
require.NoError(t, err)
assert.Equal(t, "opencode", ocBackend.Runtime.Name())

// pi is user-selectable (#6464).
piCfg := config.NewPerRepoConfig(nil, "")
piCfg.SetRuntime("pi")
Expand All @@ -89,3 +81,44 @@ func TestResolveFromPerRepoConfig(t *testing.T) {
_, err = ResolveFromPerRepoConfig(invalidCfg)
require.Error(t, err)
}

func TestResolveFromPerRepoConfig_RejectsStubRuntimes(t *testing.T) {
t.Parallel()

// Stub runtimes like "opencode" are resolvable via Resolve() for
// dev/testing, but must be rejected when coming through config.
for _, name := range []string{"opencode"} {
ocCfg := config.NewPerRepoConfig(nil, "")
ocCfg.SetRuntime(name)
_, err := ResolveFromPerRepoConfig(ocCfg)
require.Error(t, err, "stub runtime %q should fail via config path", name)
assert.Contains(t, err.Error(), "invalid runtime")
}

// Direct Resolve() still works for dev/testing.
rt, err := Resolve("opencode")
require.NoError(t, err)
assert.Equal(t, "opencode", rt.Runtime.Name())
}

func TestResolveFromConfig_RejectsStubRuntimes(t *testing.T) {
t.Parallel()

// Org config with a stub runtime should fail at resolution time.
cfg, parseErr := config.ParseOrgConfig([]byte(`version: "1"
dispatch:
platform: github-actions
defaults:
roles: [triage]
runtime: opencode
repos: {}
`))
// ParseOrgConfig calls Validate() which also rejects "opencode",
// so this may fail at parse time. If parsing succeeds (e.g. because
// Validate() is not called), ResolveFromConfig must still reject it.
if parseErr == nil {
_, err := ResolveFromConfig(cfg)
require.Error(t, err, "stub runtime %q should fail via org config path", "opencode")
assert.Contains(t, err.Error(), "invalid runtime")
}
}
Loading