208 lines
8.2 KiB
Go
208 lines
8.2 KiB
Go
package backendapp
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
|
|
"go.uber.org/zap"
|
|
|
|
"github.com/kandev/kandev/internal/agent/credentials"
|
|
"github.com/kandev/kandev/internal/agent/managedruntime"
|
|
"github.com/kandev/kandev/internal/agent/mcpconfig"
|
|
"github.com/kandev/kandev/internal/agent/registry"
|
|
agentctl "github.com/kandev/kandev/internal/agent/runtime/agentctl"
|
|
"github.com/kandev/kandev/internal/agent/runtime/lifecycle"
|
|
settingsstore "github.com/kandev/kandev/internal/agent/settings/store"
|
|
"github.com/kandev/kandev/internal/agentctl/server/process"
|
|
"github.com/kandev/kandev/internal/common/config"
|
|
"github.com/kandev/kandev/internal/common/logger"
|
|
"github.com/kandev/kandev/internal/events/bus"
|
|
"github.com/kandev/kandev/internal/secrets"
|
|
"github.com/kandev/kandev/internal/task/models"
|
|
)
|
|
|
|
func provideLifecycleManager(
|
|
ctx context.Context,
|
|
cfg *config.Config,
|
|
log *logger.Logger,
|
|
eventBus bus.EventBus,
|
|
agentSettingsRepo settingsstore.Repository,
|
|
agentRegistry *registry.Registry,
|
|
rawSecretStore secrets.SecretStore,
|
|
baseBranchProvider lifecycle.BaseBranchProvider,
|
|
comparisonTargetProvider lifecycle.ComparisonTargetProvider,
|
|
managedRuntimeSelections managedruntime.SelectionReader,
|
|
mcpIdentityScoper lifecycle.MCPIdentityScoper,
|
|
mcpPrincipalScoper lifecycle.MCPPrincipalScoper,
|
|
) (*lifecycle.Manager, error) {
|
|
log.Info("Initializing Agent Manager...")
|
|
secretStores := newLifecycleSecretStores(rawSecretStore)
|
|
|
|
// Create runtime registry to manage multiple runtimes
|
|
executorRegistry := lifecycle.NewExecutorRegistry(log)
|
|
|
|
// Standalone runtime is always available (agentctl is a core service)
|
|
controlClient := agentctl.NewControlClient(
|
|
cfg.Agent.StandaloneHost,
|
|
cfg.Agent.StandalonePort,
|
|
log,
|
|
agentctl.WithControlAuthToken(cfg.Agent.StandaloneAuthToken),
|
|
)
|
|
standaloneExec := lifecycle.NewStandaloneExecutor(
|
|
controlClient,
|
|
cfg.Agent.StandaloneHost,
|
|
cfg.Agent.StandalonePort,
|
|
log,
|
|
)
|
|
standaloneExec.SetAuthToken(cfg.Agent.StandaloneAuthToken)
|
|
|
|
// Create InteractiveRunner for passthrough mode (no WorkspaceTracker, uses callbacks)
|
|
interactiveRunner := process.NewInteractiveRunner(nil, log, 2*1024*1024) // 2MB buffer
|
|
standaloneExec.SetInteractiveRunner(interactiveRunner)
|
|
|
|
executorRegistry.Register(standaloneExec)
|
|
log.Info("Standalone runtime registered with passthrough support",
|
|
zap.String("host", cfg.Agent.StandaloneHost),
|
|
zap.Int("port", cfg.Agent.StandalonePort))
|
|
|
|
// Register Docker runtime if enabled (client is created lazily on first use)
|
|
if cfg.Docker.Enabled {
|
|
dockerExec := lifecycle.NewDockerExecutor(cfg.Docker, cfg.ResolvedHomeDir(), log)
|
|
executorRegistry.Register(dockerExec)
|
|
log.Info("Docker runtime registered (lazy initialization)")
|
|
}
|
|
|
|
// Register Remote Docker runtime (always available, instances are created lazily per host)
|
|
remoteDockerExec := lifecycle.NewRemoteDockerExecutor(log)
|
|
executorRegistry.Register(remoteDockerExec)
|
|
log.Info("Remote Docker runtime registered")
|
|
|
|
// Register Sprites runtime (remote sandboxes via Sprites.dev)
|
|
agentctlResolver := lifecycle.NewAgentctlResolver(log)
|
|
spritesExec := lifecycle.NewSpritesExecutor(secretStores.credentials, agentRegistry, agentctlResolver, 8765, log)
|
|
executorRegistry.Register(spritesExec)
|
|
log.Info("Sprites runtime registered")
|
|
|
|
// Register SSH runtime (run an agent on any Linux box reachable over SSH).
|
|
sshExec := lifecycle.NewSSHExecutor(secretStores.credentials, agentRegistry, agentctlResolver, log)
|
|
executorRegistry.Register(sshExec)
|
|
log.Info("SSH runtime registered")
|
|
registerKubernetesBackend(executorRegistry, agentctlResolver, log)
|
|
|
|
credsMgr := credentials.NewManager(log)
|
|
if secretStores.credentials != nil {
|
|
credsMgr.AddProvider(secrets.NewSecretStoreProvider(secretStores.credentials))
|
|
}
|
|
credsMgr.AddProvider(credentials.NewEnvProvider("KANDEV_"))
|
|
credsMgr.AddProvider(credentials.NewAugmentSessionProvider())
|
|
if credsFile := credentialFilePath(cfg); credsFile != "" {
|
|
credsMgr.AddProvider(credentials.NewFileProvider(credsFile))
|
|
}
|
|
|
|
profileResolver := lifecycle.NewStoreProfileResolver(agentSettingsRepo, agentRegistry)
|
|
mcpService := mcpconfig.NewService(agentSettingsRepo)
|
|
|
|
lifecycleMgr := lifecycle.NewManager(
|
|
agentRegistry,
|
|
eventBus,
|
|
executorRegistry,
|
|
credsMgr,
|
|
profileResolver,
|
|
mcpService,
|
|
lifecycle.ExecutorFallbackWarn,
|
|
cfg.ResolvedHomeDir(),
|
|
log,
|
|
)
|
|
|
|
// Register environment preparers (keyed by ExecutorType — the
|
|
// "local"/"worktree"/"local_docker"/"sprites" taxonomy, not Runtime).
|
|
// The Worktree preparer is registered separately in
|
|
// Manager.SetWorktreeManager once a worktree.Manager is wired.
|
|
preparerRegistry := lifecycle.NewPreparerRegistry(log)
|
|
localPreparer := lifecycle.NewLocalPreparer(log)
|
|
preparerRegistry.Register(models.ExecutorTypeLocal, localPreparer)
|
|
preparerRegistry.Register(models.ExecutorTypeMockRemote, localPreparer)
|
|
preparerRegistry.Register(models.ExecutorTypeLocalDocker, lifecycle.NewDockerPreparer(log))
|
|
preparerRegistry.Register(models.ExecutorTypeSprites, lifecycle.NewSpritesPreparer(log))
|
|
preparerRegistry.Register(models.ExecutorTypeSSH, lifecycle.NewSSHPreparer(log))
|
|
registerKubernetesPreparer(preparerRegistry, log)
|
|
lifecycleMgr.SetPreparerRegistry(preparerRegistry)
|
|
lifecycleMgr.SetSecretStore(secretStores.runtime)
|
|
if err := lifecycleMgr.SetAgentctlStartupConfig(cfg.ManagedAgentctlStartupConfig()); err != nil {
|
|
return nil, fmt.Errorf("configure managed agentctl startup: %w", err)
|
|
}
|
|
// Record the standalone agentctl control-server PID (populated by
|
|
// provideAgentctlLauncher, which runs before this) so local/standalone
|
|
// executor rows carry a real host-local liveness handle.
|
|
lifecycleMgr.SetStandaloneHostPID(cfg.Agent.StandalonePID)
|
|
// Wire the agent_profiles reader so the launch-prep skill deploy hook
|
|
// (ADR 0005 Wave A) can resolve full profile rows including the office
|
|
// enrichment fields. Without a wired SkillDeployer this is a no-op,
|
|
// but the reader still lets future Wave-B/C consumers light up.
|
|
lifecycleMgr.SetAgentProfileReader(agentSettingsRepo)
|
|
lifecycleMgr.SetManagedRuntimeSelectionStore(managedRuntimeSelections)
|
|
|
|
// MCP handler is set later in main.go after MCP handlers are registered
|
|
// via lifecycleMgr.SetMCPHandler(gateway.Dispatcher)
|
|
// Wire the base-branch provider before Start so recovered executions are
|
|
// seeded during startup as well as newly-created executions.
|
|
lifecycleMgr.SetBaseBranchProvider(baseBranchProvider)
|
|
// Wire the comparison-target provider before Start so recovered executions
|
|
// hydrate the exact provider-qualified ref before their first status poll.
|
|
lifecycleMgr.SetComparisonTargetProvider(comparisonTargetProvider)
|
|
// Recovered executions can dispatch MCP calls as soon as Start resumes
|
|
// their streams. Install both trusted scopes before Start, not after route
|
|
// registration, so the first recovered call has the same authority boundary
|
|
// as every later call.
|
|
if mcpIdentityScoper != nil {
|
|
lifecycleMgr.SetMCPIdentityScoper(mcpIdentityScoper)
|
|
}
|
|
if mcpPrincipalScoper != nil {
|
|
lifecycleMgr.SetMCPPrincipalScoper(mcpPrincipalScoper)
|
|
}
|
|
|
|
if err := lifecycleMgr.Start(ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
log.Info("Agent Manager initialized",
|
|
zap.Int("runtimes", len(executorRegistry.List())),
|
|
zap.Int("agent_types", len(agentRegistry.List())))
|
|
return lifecycleMgr, nil
|
|
}
|
|
|
|
type lifecycleSecretStores struct {
|
|
runtime secrets.SecretStore
|
|
credentials secrets.SecretStore
|
|
}
|
|
|
|
func newLifecycleSecretStores(raw secrets.SecretStore) lifecycleSecretStores {
|
|
return lifecycleSecretStores{
|
|
runtime: raw,
|
|
credentials: secrets.NewUserVisibleStore(raw),
|
|
}
|
|
}
|
|
|
|
func registerKubernetesBackend(
|
|
registry *lifecycle.ExecutorRegistry,
|
|
agentctlResolver *lifecycle.AgentctlResolver,
|
|
log *logger.Logger,
|
|
) {
|
|
registry.Register(lifecycle.NewKubernetesExecutor(agentctlResolver, log))
|
|
log.Info("Kubernetes runtime registered (lazy initialization)")
|
|
}
|
|
|
|
func registerKubernetesPreparer(registry *lifecycle.PreparerRegistry, log *logger.Logger) {
|
|
registry.Register(models.ExecutorTypeKubernetes, lifecycle.NewKubernetesPreparer(log))
|
|
log.Info("Kubernetes preparer registered")
|
|
}
|
|
|
|
func credentialFilePath(cfg *config.Config) string {
|
|
if cfg != nil {
|
|
return strings.TrimSpace(cfg.Credentials.File)
|
|
}
|
|
return strings.TrimSpace(os.Getenv("KANDEV_CREDENTIALS_FILE"))
|
|
}
|