Files
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

800 lines
31 KiB
Go

package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"log/slog"
"net"
"net/http"
"os"
"path/filepath"
"strings"
"sync"
"time"
agentdaemon "github.com/tutti-os/tutti/packages/agent/daemon"
agenthttpx "github.com/tutti-os/tutti/packages/agent/daemon/httpx"
agentruntime "github.com/tutti-os/tutti/packages/agent/daemon/runtime"
runtimeprep "github.com/tutti-os/tutti/packages/agent/runtimeprep"
connectorcontrolplane "github.com/tutti-os/tutti/packages/connector/daemon/adapters/controlplane"
connectormarketdata "github.com/tutti-os/tutti/packages/connector/daemon/adapters/sqlite"
connectormarketdaemon "github.com/tutti-os/tutti/packages/connector/daemon/application"
connectorcatalog "github.com/tutti-os/tutti/packages/connector/daemon/application/adapters/catalog"
connectormarkethost "github.com/tutti-os/tutti/packages/connector/daemon/core"
connectorruntime "github.com/tutti-os/tutti/packages/connector/runtime"
connectoragentgateway "github.com/tutti-os/tutti/packages/connector/runtime/agentgateway"
marketartifact "github.com/tutti-os/tutti/packages/connector/runtime/artifact"
tuttiapi "github.com/tutti-os/tutti/services/tuttid/api"
preferencesbiz "github.com/tutti-os/tutti/services/tuttid/biz/preferences"
workspacedata "github.com/tutti-os/tutti/services/tuttid/data/workspace"
tuttiserver "github.com/tutti-os/tutti/services/tuttid/server"
accountservice "github.com/tutti-os/tutti/services/tuttid/service/account"
accountrealtimeservice "github.com/tutti-os/tutti/services/tuttid/service/accountrealtime"
agentservice "github.com/tutti-os/tutti/services/tuttid/service/agent"
agentextensionservice "github.com/tutti-os/tutti/services/tuttid/service/agentextension"
agentstatusservice "github.com/tutti-os/tutti/services/tuttid/service/agentstatus"
browsersvc "github.com/tutti-os/tutti/services/tuttid/service/browser"
cliservice "github.com/tutti-os/tutti/services/tuttid/service/cli"
computersvc "github.com/tutti-os/tutti/services/tuttid/service/computer"
connectormarketservice "github.com/tutti-os/tutti/services/tuttid/service/connector/market"
connectormcpservice "github.com/tutti-os/tutti/services/tuttid/service/connector/mcp"
desktopupdateadmissionservice "github.com/tutti-os/tutti/services/tuttid/service/desktopupdateadmission"
devicepresenceservice "github.com/tutti-os/tutti/services/tuttid/service/devicepresence"
eventstreamservice "github.com/tutti-os/tutti/services/tuttid/service/eventstream"
managedruntimeservice "github.com/tutti-os/tutti/services/tuttid/service/managedruntime"
mobileremoteservice "github.com/tutti-os/tutti/services/tuttid/service/mobileremote"
modelgatewayservice "github.com/tutti-os/tutti/services/tuttid/service/modelgateway"
preferencesservice "github.com/tutti-os/tutti/services/tuttid/service/preferences"
reporterservice "github.com/tutti-os/tutti/services/tuttid/service/reporter"
tuttimodeexecutionservice "github.com/tutti-os/tutti/services/tuttid/service/tuttimodeexecution"
userpresenceservice "github.com/tutti-os/tutti/services/tuttid/service/userpresence"
workspaceservice "github.com/tutti-os/tutti/services/tuttid/service/workspace"
tuttitypes "github.com/tutti-os/tutti/services/tuttid/types"
)
const connectorMarketDefaultBaseURL = "https://api.tutti.sh/api/desktop"
const connectorMCPDefaultBaseURL = "https://tutti.sh/api/desktop"
const connectorArtifactBaseURL = "https://d27a59zdy4534h.cloudfront.net/tutti/connector-market/"
type tuttiWiring struct {
api tuttiapi.DaemonAPI
appCenterService *workspaceservice.AppCenterService
workspaceStore *workspacedata.SQLiteStore
connectorMarketStore *connectormarketdata.Store
connectorMarketHost *connectormarketdaemon.Host
connectorMCPServer *connectormcpservice.Server
connectorAgentGateway *connectoragentgateway.Gateway
analyticsReporter reporterservice.Reporter
browserService *browsersvc.Service
computerService *computersvc.Service
desktopUpdateAdmission *desktopupdateadmissionservice.Service
devicePresence *devicepresenceservice.Service
accountRealtime *accountrealtimeservice.Service
userPresence *userpresenceservice.Service
userPresenceEventsCancel context.CancelFunc
agentTargetSetup *agentextensionservice.SetupService
agentRuntime *agentdaemon.Runtime
providerAuthWatcher *agentservice.ProviderAuthWatcher
agentCLIUpdateScheduler *agentstatusservice.ProviderUpdateScheduler
tuttiModeWakeRecoveryStarter func()
tuttiModeWatchdogMu sync.Mutex
tuttiModeWatchdogWorker *tuttimodeexecutionservice.Worker
tuttiModeWatchdogCancel context.CancelFunc
tuttiModeWatchdogDone <-chan struct{}
tuttiModeWatchdogClosed bool
mobileRemoteHost mobileRemoteHost
mobileRemoteHandler http.Handler
modelGateway *modelgatewayservice.Gateway
}
type mobileRemoteHost interface {
StartRemoteHost(http.Handler)
StopRemoteHost()
}
type analyticsDebugEventPublisher struct {
service analyticsDebugEventStream
}
type analyticsDebugEventStream interface {
PublishFromServer(context.Context, string, []byte) error
}
type analyticsDebugReportedPayload struct {
Events []analyticsDebugReportedEventPayload `json:"events"`
}
type analyticsDebugReportedEventPayload struct {
Name string `json:"name"`
ClientTS int64 `json:"clientTs"`
Params map[string]any `json:"params"`
}
func (p analyticsDebugEventPublisher) PublishAnalyticsDebugEvents(ctx context.Context, events []reporterservice.DebugEvent) {
if p.service == nil || len(events) == 0 {
return
}
payload := analyticsDebugReportedPayload{
Events: make([]analyticsDebugReportedEventPayload, 0, len(events)),
}
for _, event := range events {
payload.Events = append(payload.Events, analyticsDebugReportedEventPayload{
Name: event.Name,
ClientTS: event.ClientTS,
Params: event.Params,
})
}
encoded, err := json.Marshal(payload)
if err != nil {
return
}
_ = p.service.PublishFromServer(ctx, eventstreamservice.TopicAnalyticsDebugReported, encoded)
}
func newTuttiWiring() (*tuttiWiring, error) {
wiring := &tuttiWiring{}
desktopUpdateAdmission, err := desktopupdateadmissionservice.NewFromEnvironment()
if err != nil {
return nil, fmt.Errorf("configure desktop update admission: %w", err)
}
wiring.desktopUpdateAdmission = desktopUpdateAdmission
if desktopUpdateAdmission != nil {
desktopUpdateAdmission.Start(context.Background())
}
if err := wiring.buildWorkspaceModule(context.Background()); err != nil {
_ = wiring.Close()
return nil, err
}
return wiring, nil
}
func buildTuttiServer() (*http.Server, net.Listener, *tuttiWiring, error) {
listenerSpec, err := tuttiserver.ListenerSpecFromEnv()
if err != nil {
return nil, nil, nil, fmt.Errorf("resolve tuttid listener spec: %w", err)
}
listener, err := tuttiserver.NewListener(listenerSpec)
if err != nil {
return nil, nil, nil, fmt.Errorf("create tuttid listener: %w", err)
}
if err := tuttiserver.WriteListenerInfo(listener, listenerSpec); err != nil {
_ = listener.Close()
return nil, nil, nil, fmt.Errorf("write tuttid listener info: %w", err)
}
slog.Info("tuttid listener allocated",
"event", "tutti.listen.allocated",
"addr", listener.Addr().String(),
)
wiringStartedAt := time.Now()
slog.Info("tuttid wiring build started", "event", "tutti.wiring.build_started")
wiring, err := newTuttiWiring()
if err != nil {
_ = listener.Close()
_ = os.Remove(tuttitypes.TuttidListenerInfoPath())
return nil, nil, nil, err
}
slog.Info("tuttid wiring build completed",
"event", "tutti.wiring.build_completed",
"durationMs", time.Since(wiringStartedAt).Milliseconds(),
)
wiring.startTuttiModeWakeRecovery()
wiring.startAgentCLIUpdateScheduler()
routes := wiring.routes()
wiring.startMobileRemoteHost(tuttiserver.NewMux(routes))
return tuttiserver.NewHTTPServer(listenerSpec, routes), listener, wiring, nil
}
func (w *tuttiWiring) routes() tuttiserver.Routes {
return tuttiapi.NewRoutes(w.api)
}
func (w *tuttiWiring) buildWorkspaceModule(ctx context.Context) error {
workspaceStore, err := openWorkspaceStore(ctx)
if err != nil {
return err
}
w.workspaceStore = workspaceStore
// Browser use is delivered through the daemon-owned `tutti browser` CLI;
// the service owns a chrome-devtools-mcp subprocess per workspace.
if runtimeprep.BrowserUseDefaultEnabled() {
w.browserService = browsersvc.NewService(workspaceStore)
}
// Computer use is delivered through the daemon-owned `tutti computer` CLI;
// the service owns a cua-driver MCP subprocess per workspace.
if runtimeprep.ComputerUseDefaultEnabled() {
w.computerService = computersvc.NewService()
}
modelGateway, err := modelgatewayservice.New(modelgatewayservice.Config{})
if err != nil {
return fmt.Errorf("start model gateway: %w", err)
}
w.modelGateway = modelGateway
connectorsEnabled, err := connectorModuleEnabled(ctx, workspaceStore)
if err != nil {
slog.Warn(
"connector module remains disabled because desktop preferences could not be read",
"event", "tutti.connector.activation.preference_read_failed",
"error", err,
)
connectorsEnabled = false
}
var connectorRegistry *connectormarketservice.ConnectorRuntimeRegistry
var connectorCommands *connectormarketservice.ConnectorCommandProvider
var connectorAgent *connectorAgentRuntime
var agentConnectorRuntime agentservice.ConnectorRuntime
if connectorsEnabled {
connectorRegistry = connectormarketservice.NewConnectorRuntimeRegistry()
connectorCommands = connectormarketservice.NewConnectorCommandProvider(connectorRegistry)
connectorMCPServer, startErr := connectormcpservice.Start(connectormcpservice.Config{Registry: connectorRegistry.MCPRegistry()})
if startErr != nil {
return fmt.Errorf("start connector MCP server: %w", startErr)
}
w.connectorMCPServer = connectorMCPServer
connectorGateway, startErr := connectoragentgateway.Start(connectoragentgateway.Config{})
if startErr != nil {
return fmt.Errorf("start connector Agent gateway: %w", startErr)
}
if bindErr := connectorGateway.SetBackend(fmt.Sprintf("tuttid-%d", time.Now().UnixNano()), connectorMCPServer); bindErr != nil {
_ = connectorGateway.Close(context.Background())
return fmt.Errorf("bind connector MCP backend: %w", bindErr)
}
w.connectorAgentGateway = connectorGateway
connectorAgent = &connectorAgentRuntime{routes: connectorRegistry.RouteRegistry(), server: connectorGateway,
cliBinDir: filepath.Join(tuttitypes.DefaultStateDir(), "connectors", "user-state", "bin")}
agentConnectorRuntime = connectorAgent
}
api, appCenterService, agentRuntime, providerAuthWatcher, err := buildDaemonAPI(
ctx, workspaceStore, nil, w.browserService, w.computerService,
modelGateway, agentConnectorRuntime, w.installTuttiModeWatchdogWorker,
)
if err != nil {
_ = modelGateway.Close()
w.modelGateway = nil
return err
}
accountService, accountOK := api.AccountService.(*accountservice.Service)
if !accountOK || accountService == nil {
agentRuntime.Close()
providerAuthWatcher.Close()
return errors.New("account realtime session adapter is unavailable")
}
if err := w.configureAccountRealtime(&api, accountService); err != nil {
agentRuntime.Close()
providerAuthWatcher.Close()
return err
}
if connectorsEnabled {
connectorMarketStore, err := connectormarketdata.Open(ctx, workspacedata.DefaultDBPath())
if err != nil {
agentRuntime.Close()
providerAuthWatcher.Close()
return fmt.Errorf("open connector market store: %w", err)
}
connectorMarketBaseURL := strings.TrimSpace(os.Getenv("TUTTI_CONNECTOR_MARKET_BASE_URL"))
if connectorMarketBaseURL == "" {
connectorMarketBaseURL = connectorMarketDefaultBaseURL
}
connectorMarketType := strings.ToLower(strings.TrimSpace(os.Getenv("TUTTI_CONNECTOR_MARKET_TYPE")))
if connectorMarketType == "" {
connectorMarketType = "overseas"
}
marketAuthorizer, err := connectormarketservice.NewAccountSessionAuthorizer(
accountService.AuthJSONPath,
os.Getenv("TUTTI_PPE_LANE"),
)
if err != nil {
return fmt.Errorf("configure connector market account authorization: %w", err)
}
connectorCatalog, err := connectorcatalog.NewCatalogSource(connectorcatalog.CatalogSourceConfig{
BaseURL: connectorMarketBaseURL,
ExpectedMarketType: connectorMarketType,
HTTPClient: agenthttpx.NewClient(30 * time.Second),
AuthorizeRequest: marketAuthorizer.Authorize,
})
if err != nil {
_ = connectorMarketStore.Close()
agentRuntime.Close()
providerAuthWatcher.Close()
return fmt.Errorf("configure connector market catalog: %w", err)
}
events, eventsOK := api.EventStreamService.(*eventstreamservice.Service)
if !eventsOK {
_ = connectorMarketStore.Close()
agentRuntime.Close()
providerAuthWatcher.Close()
return errors.New("connector market event stream wiring is invalid")
}
artifactBaseURL := strings.TrimSpace(os.Getenv("TUTTI_CONNECTOR_ARTIFACT_BASE_URL"))
if artifactBaseURL == "" {
artifactBaseURL = connectorArtifactBaseURL
}
artifactFetcher, err := marketartifact.NewDirectFetcher(marketartifact.DirectFetcherConfig{
BaseURL: artifactBaseURL, HTTPClient: agenthttpx.NewClient(5 * time.Minute),
})
if err != nil {
return fmt.Errorf("configure connector artifact download: %w", err)
}
connectorStateRoot := filepath.Join(tuttitypes.DefaultStateDir(), "connectors")
artifactPreparer, err := marketartifact.NewPreparer(marketartifact.Config{RootDir: filepath.Join(connectorStateRoot, "artifacts"), Fetcher: artifactFetcher})
if err != nil {
return fmt.Errorf("configure connector artifact preparer: %w", err)
}
runtimeResolver, err := managedruntimeservice.NewConnectorRuntimeResolver(managedruntimeservice.ConnectorRuntimeResolverConfig{
Resolver: managedruntimeservice.DefaultResolver{},
})
if err != nil {
return fmt.Errorf("configure connector managed runtime: %w", err)
}
processTransport, err := agentruntime.NewConnectorProcessTransport()
if err != nil {
return fmt.Errorf("configure connector process transport: %w", err)
}
nodePackageInstaller, err := connectorruntime.NewNodePackageInstaller(connectorruntime.NodePackageInstallerConfig{
RootDir: filepath.Join(connectorStateRoot, "node-packages"), Runtimes: runtimeResolver, Processes: processTransport,
})
if err != nil {
return fmt.Errorf("configure connector node package installer: %w", err)
}
releaseInstaller, err := connectorruntime.NewReleaseInstaller(artifactPreparer, nodePackageInstaller)
if err != nil {
return fmt.Errorf("configure connector release installer: %w", err)
}
userHome, err := os.UserHomeDir()
if err != nil || !filepath.IsAbs(userHome) {
return errors.New("configure connector implementation host: user home is unavailable")
}
connectorMCPBaseURL := strings.TrimSpace(os.Getenv("TUTTI_CONNECTOR_MCP_BASE_URL"))
if connectorMCPBaseURL == "" {
connectorMCPBaseURL = connectorMCPDefaultBaseURL
}
remoteMCPClientFactory, err := connectormarketservice.NewDirectRemoteMCPClientFactory(connectormarketservice.DirectRemoteMCPClientFactoryConfig{
BaseURL: connectorMCPBaseURL, HTTPClient: agenthttpx.NewClient(2 * time.Minute),
Timeout: 30 * time.Second, MaxResponseBytes: 4 * 1024 * 1024,
AuthorizeAccountRequest: marketAuthorizer.AuthorizeForAccount,
})
if err != nil {
return fmt.Errorf("configure remote connector MCP client factory: %w", err)
}
implementationHost, err := connectormarketservice.NewImplementationHost(connectormarketservice.ImplementationHostConfig{
Artifacts: artifactPreparer, CLIInstallations: nodePackageInstaller,
Runtimes: runtimeResolver, Processes: processTransport, Registry: connectorRegistry,
RemoteMCPClientFactory: remoteMCPClientFactory,
StateRoot: filepath.Join(connectorStateRoot, "user-state"),
BinDir: filepath.Join(tuttitypes.DefaultStateDir(), "bin"),
UserHome: userHome,
})
if err != nil {
return fmt.Errorf("configure connector implementation host: %w", err)
}
connectorAuthorizationClient, err := connectorcontrolplane.NewAuthorizationClient(connectorcontrolplane.AuthorizationClientConfig{
BaseURL: connectorMarketBaseURL, APIPrefix: "/v1", HTTPClient: agenthttpx.NewClient(30 * time.Second),
AuthorizeAccountRequest: marketAuthorizer.AuthorizeForAccount,
})
if err != nil {
return fmt.Errorf("configure connector authorization: %w", err)
}
connectorAuthorizationEvents := accountrealtimeservice.ConnectorAuthorizationEventSource{Realtime: w.accountRealtime}
connectorRuntime, connectorAuthorization, compatibility, implementations := connectormarketservice.ProductionPorts(implementationHost, connectorAuthorizationClient)
if api.CLIRegistry == nil {
return errors.New("connector command registry cannot attach to daemon CLI")
}
api.CLIRegistry.AppCommands = cliservice.CompositeDynamicCommandRegistry{Registries: []cliservice.DynamicCommandRegistry{
api.CLIRegistry.AppCommands, connectorCommands,
}}
connectorAuthorizationReadiness := connectormarkethost.NewAuthorizationReadinessGate()
connectorMarketScope := func() connectormarkethost.OperationScope {
session, sessionErr := accountService.ReadSession()
if sessionErr != nil || session == nil {
return connectormarkethost.OperationScope{}
}
return connectormarkethost.OperationScope{AccountID: strings.TrimSpace(session.UserID)}
}
connectorMarketHost, err := connectormarketdaemon.NewHost(ctx, connectormarketdaemon.HostConfig{
Repository: connectorMarketStore, CatalogSource: connectorCatalog,
ReleaseInstallations: releaseInstaller, ImplementationHost: connectorRuntime,
Authorization: connectorAuthorization, Compatibility: compatibility,
AuthorizationProjections: connectorMarketStore,
AuthorizationSnapshots: connectorAuthorizationClient,
AuthorizationEvents: connectorAuthorizationEvents,
AuthorizationReadiness: connectorAuthorizationReadiness,
RuntimeBindings: connectormarkethost.AccountRuntimeBindingResolver{Projections: connectorMarketStore, Readiness: connectorAuthorizationReadiness},
ImplementationRegistry: implementations, Outbox: connectorMarketStore, Lifecycle: connectorMarketStore,
Publisher: eventstreamservice.ConnectorMarketPublisher{Service: events, CurrentScope: connectorMarketScope},
})
if err != nil {
_ = connectorMarketStore.Close()
agentRuntime.Close()
providerAuthWatcher.Close()
return fmt.Errorf("start connector market host: %w", err)
}
if service, ok := api.AgentSessionService.(*agentservice.Service); ok {
service.ConnectorMarketSnapshots = connectorMarketHost.Application
service.ConnectorMarketCurrentScope = connectorMarketScope
}
api.ConnectorMarketService = connectorMarketHost.Application
api.ConnectorMarketScope = connectorMarketScope
api.ConnectorAuthorizationReady = connectorAuthorizationReadiness.Ready
existingAccountLoginCompleted := accountService.OnLoginCompleted
accountService.OnLoginCompleted = func(loginContext context.Context) {
if existingAccountLoginCompleted != nil {
existingAccountLoginCompleted(loginContext)
}
go bootstrapConnectorMarket(connectorMarketHost, connectorMarketScope)
}
existingAccountLogoutCompleted := accountService.OnLogoutCompleted
accountService.OnLogoutCompleted = func(logoutContext context.Context) {
if existingAccountLogoutCompleted != nil {
existingAccountLogoutCompleted(logoutContext)
}
connectorAgent.RevokeAll()
fenceContext, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if fenceErr := connectorMarketHost.FenceForScope(fenceContext, connectormarkethost.OperationScope{}); fenceErr != nil {
slog.Warn("connector market logout fence failed", "error", fenceErr)
}
}
w.connectorMarketStore = connectorMarketStore
w.connectorMarketHost = connectorMarketHost
connectorRegistry.MCPRegistry().SetAuthorizationErrorObserver(connectorMarketHost.NotifyAuthorizationChanged)
startExistingListenerWork := api.OnListenerReady
api.OnListenerReady = func() {
if startExistingListenerWork != nil {
startExistingListenerWork()
}
go bootstrapConnectorMarket(connectorMarketHost, connectorMarketScope)
}
}
agentTargetSetup, ok := api.AgentTargetSetupService.(*agentextensionservice.SetupService)
if !ok {
agentRuntime.Close()
providerAuthWatcher.Close()
return errors.New("agent target setup service wiring is invalid")
}
w.agentTargetSetup = agentTargetSetup
w.agentRuntime = agentRuntime
w.providerAuthWatcher = providerAuthWatcher
mobileRemoteService, mobileRemoteOK := api.MobileRemoteService.(*mobileremoteservice.Service)
if !mobileRemoteOK {
return errors.New("mobile remote service wiring is invalid")
}
w.mobileRemoteHost = mobileRemoteService
preferencesService, preferencesOK := api.PreferencesService.(*preferencesservice.Service)
agentUpdateDiscoverer, agentUpdateDiscovererOK := api.AgentStatusService.(agentstatusservice.ManagedProviderUpdateDiscoverer)
if !preferencesOK || !agentUpdateDiscovererOK {
return errors.New("agent CLI update scheduler wiring is invalid")
}
w.agentCLIUpdateScheduler = agentstatusservice.NewProviderUpdateScheduler(
agentstatusservice.ProviderUpdateSchedulerConfig{Discoverer: agentUpdateDiscoverer},
)
w.observeDesktopPreferenceChanges(preferencesService)
analyticsConfig := tuttitypes.ResolveAnalyticsConfig()
debugPublisher := resolveAnalyticsDebugPublisher(analyticsConfig, api.EventStreamService)
var dynamicContextProvider func() reporterservice.DynamicContext
if account, ok := api.AccountService.(*accountservice.Service); ok {
dynamicContextProvider = account.AnalyticsContext
}
analyticsReporter, err := reporterservice.New(reporterservice.Config{
Analytics: analyticsConfig,
DebugPublisher: debugPublisher,
StateDir: tuttitypes.DefaultStateDir(),
CommonParams: map[string]any{
"authority": "client",
"business_app_id": "233749",
"client": "desktop",
"environment": tuttitypes.ResolveDefaultsFromEnv().Runtime.Env,
"schema_version": 1,
},
DynamicContextProvider: dynamicContextProvider,
})
if err != nil {
return fmt.Errorf("create analytics reporter: %w", err)
}
attachAnalyticsReporter(&api, analyticsReporter)
w.analyticsReporter = analyticsReporter
w.api = api
w.api.DesktopUpdateAdmissionService = w.desktopUpdateAdmission
w.appCenterService = appCenterService
w.tuttiModeWakeRecoveryStarter = api.OnListenerReady
return nil
}
type desktopPreferencesReader interface {
GetDesktopPreferences(context.Context) (preferencesbiz.DesktopPreferences, error)
}
func connectorModuleEnabled(ctx context.Context, preferences desktopPreferencesReader) (bool, error) {
if preferences == nil {
return false, errors.New("desktop preferences reader is unavailable")
}
current, err := preferences.GetDesktopPreferences(ctx)
if err != nil {
return false, err
}
return preferencesbiz.IsLabFlagEnabled(current.FeatureFlags, preferencesbiz.LabFlagConnectors), nil
}
func (w *tuttiWiring) observeDesktopPreferenceChanges(preferences *preferencesservice.Service) {
if w == nil || preferences == nil {
return
}
preferences.RegisterChangeObserver(func(_ context.Context, previous, current preferencesbiz.DesktopPreferences) {
if w.agentCLIUpdateScheduler != nil && previous.AgentCLIUpdateCheckEnabled != current.AgentCLIUpdateCheckEnabled {
w.agentCLIUpdateScheduler.SetEnabled(current.AgentCLIUpdateCheckEnabled)
}
})
preferences.RegisterChangeObserver(func(_ context.Context, previous, current preferencesbiz.DesktopPreferences) {
previousMobileRemoteEnabled := preferencesbiz.IsCapabilityFlagEnabled(
previous.FeatureFlags,
preferencesbiz.FeatureFlagMobileRemoteAccess,
)
currentMobileRemoteEnabled := preferencesbiz.IsCapabilityFlagEnabled(
current.FeatureFlags,
preferencesbiz.FeatureFlagMobileRemoteAccess,
)
if previousMobileRemoteEnabled != currentMobileRemoteEnabled {
w.setMobileRemoteAccessEnabled(currentMobileRemoteEnabled)
}
})
}
func (w *tuttiWiring) startTuttiModeWakeRecovery() {
if w == nil {
return
}
w.tuttiModeWatchdogMu.Lock()
if w.tuttiModeWatchdogClosed ||
w.tuttiModeWakeRecoveryStarter == nil {
w.tuttiModeWatchdogMu.Unlock()
return
}
start := w.tuttiModeWakeRecoveryStarter
w.tuttiModeWakeRecoveryStarter = nil
w.tuttiModeWatchdogMu.Unlock()
start()
w.tuttiModeWatchdogMu.Lock()
defer w.tuttiModeWatchdogMu.Unlock()
if w.tuttiModeWatchdogClosed ||
w.tuttiModeWatchdogWorker == nil ||
w.tuttiModeWatchdogDone != nil {
return
}
workerCtx, cancel := context.WithCancel(context.Background())
w.tuttiModeWatchdogCancel = cancel
w.tuttiModeWatchdogDone = startTuttiModeWatchdogWorker(
workerCtx, *w.tuttiModeWatchdogWorker,
)
}
func (w *tuttiWiring) installTuttiModeWatchdogWorker(
worker tuttimodeexecutionservice.Worker,
) {
if w == nil {
return
}
w.tuttiModeWatchdogMu.Lock()
defer w.tuttiModeWatchdogMu.Unlock()
if w.tuttiModeWatchdogClosed {
return
}
w.tuttiModeWatchdogWorker = &worker
}
func (w *tuttiWiring) stopTuttiModeWatchdogWorker() {
if w == nil {
return
}
w.tuttiModeWatchdogMu.Lock()
w.tuttiModeWatchdogClosed = true
cancel := w.tuttiModeWatchdogCancel
done := w.tuttiModeWatchdogDone
w.tuttiModeWatchdogMu.Unlock()
if cancel != nil {
cancel()
}
if done != nil {
<-done
}
}
func (w *tuttiWiring) startAgentCLIUpdateScheduler() {
if w == nil || w.agentCLIUpdateScheduler == nil || w.api.PreferencesService == nil {
return
}
preferences, err := w.api.PreferencesService.Get(context.Background())
if err != nil {
slog.Warn("failed to read agent CLI update check preference",
"event", "tutti.agent_provider.update_scheduler.preference_read_failed",
"error", err,
)
w.agentCLIUpdateScheduler.Start(false)
return
}
w.agentCLIUpdateScheduler.Start(preferences.AgentCLIUpdateCheckEnabled)
}
func (w *tuttiWiring) startMobileRemoteHost(handler http.Handler) {
if w == nil || w.mobileRemoteHost == nil || handler == nil || w.api.PreferencesService == nil {
return
}
w.mobileRemoteHandler = handler
enabled := false
preferences, err := w.api.PreferencesService.Get(context.Background())
if err != nil {
slog.Warn(
"failed to read mobile remote access preference",
"event", "tutti.mobile_remote.preference_read_failed",
"error", err,
)
} else {
enabled = preferencesbiz.IsCapabilityFlagEnabled(
preferences.FeatureFlags,
preferencesbiz.FeatureFlagMobileRemoteAccess,
)
}
w.setMobileRemoteAccessEnabled(enabled)
}
func (w *tuttiWiring) setMobileRemoteAccessEnabled(enabled bool) {
if w == nil || w.mobileRemoteHost == nil {
return
}
if enabled {
w.mobileRemoteHost.StartRemoteHost(w.mobileRemoteHandler)
return
}
w.mobileRemoteHost.StopRemoteHost()
}
func resolveAnalyticsDebugPublisher(analyticsConfig tuttitypes.AnalyticsConfig, service analyticsDebugEventStream) reporterservice.DebugPublisher {
if analyticsConfig.Disabled || service == nil {
return nil
}
return analyticsDebugEventPublisher{
service: service,
}
}
func attachAnalyticsReporter(api *tuttiapi.DaemonAPI, analyticsReporter reporterservice.Reporter) {
if api == nil {
return
}
api.AnalyticsReporter = analyticsReporter
if service, ok := api.AgentSessionService.(*agentservice.Service); ok {
service.AnalyticsReporter = analyticsReporter
if projection, ok := service.SessionReader.(*agentservice.ActivityProjection); ok {
projection.SetAnalyticsReporter(analyticsReporter)
}
}
if service, ok := api.AgentStatusService.(*agentstatusservice.Service); ok {
service.AnalyticsReporter = analyticsReporter
}
if service, ok := api.AccountService.(*accountservice.Service); ok {
service.SetAnalyticsReporter(analyticsReporter)
}
if service, ok := api.PreferencesService.(*preferencesservice.Service); ok {
service.AnalyticsReporter = analyticsReporter
}
}
func openWorkspaceStore(ctx context.Context) (*workspacedata.SQLiteStore, error) {
workspaceStore, err := workspacedata.OpenSQLiteStore(workspacedata.DefaultDBPath())
if err != nil {
return nil, fmt.Errorf("open workspace database: %w", err)
}
if err := workspaceStore.Migrate(ctx); err != nil {
_ = workspaceStore.Close()
return nil, fmt.Errorf("migrate workspace database: %w", err)
}
return workspaceStore, nil
}
func bootstrapConnectorMarket(host *connectormarketdaemon.Host, scope func() connectormarkethost.OperationScope) {
if host == nil {
return
}
ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second)
defer cancel()
operationScope := connectormarkethost.OperationScope{}
if scope != nil {
operationScope = scope()
}
err := host.BootstrapForScope(ctx, operationScope)
if err != nil {
slog.Warn("connector market bootstrap failed; routes remain fenced", "error", err)
}
}
func (w *tuttiWiring) Close() error {
if w == nil {
return nil
}
w.stopTuttiModeWatchdogWorker()
if w.desktopUpdateAdmission != nil {
w.desktopUpdateAdmission.Close()
}
var closeErr error
if w.mobileRemoteHost != nil {
w.mobileRemoteHost.StopRemoteHost()
}
w.stopAccountRealtime()
if w.agentCLIUpdateScheduler != nil {
w.agentCLIUpdateScheduler.Close()
}
if w.appCenterService != nil && w.appCenterService.Runner != nil {
w.appCenterService.Runner.StopAll(context.Background())
}
if w.appCenterService != nil {
w.appCenterService.StopWorkspaceAppUploadJanitor()
}
if w.browserService != nil {
w.browserService.Close()
}
if w.computerService != nil {
w.computerService.Close()
}
if w.providerAuthWatcher != nil {
w.providerAuthWatcher.Close()
}
if w.agentTargetSetup != nil {
if err := w.agentTargetSetup.Close(); err != nil {
closeErr = err
}
}
if w.agentRuntime != nil {
w.agentRuntime.Close()
}
if w.modelGateway != nil {
if err := w.modelGateway.Close(); err != nil && closeErr == nil {
closeErr = err
}
}
if w.connectorMarketHost != nil {
w.connectorMarketHost.Close()
}
// Stop the stable Agent-facing listener before retiring its replaceable
// backend so no new request can race backend shutdown.
if w.connectorAgentGateway != nil {
closeCtx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
if err := w.connectorAgentGateway.Close(closeCtx); err != nil && closeErr == nil {
closeErr = err
}
cancel()
}
if w.connectorMCPServer != nil {
closeCtx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
if err := w.connectorMCPServer.Close(closeCtx); err != nil && closeErr == nil {
closeErr = err
}
cancel()
}
if w.connectorMarketStore != nil {
if err := w.connectorMarketStore.Close(); err != nil && closeErr == nil {
closeErr = err
}
}
if w.analyticsReporter != nil {
if err := w.analyticsReporter.Close(); err != nil && closeErr == nil {
closeErr = err
}
}
if w.workspaceStore == nil {
return closeErr
}
if err := w.workspaceStore.Close(); err != nil && closeErr == nil {
closeErr = err
}
return closeErr
}