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
844 lines
32 KiB
Go
844 lines
32 KiB
Go
// Package implementationhost owns the host-neutral managed Connector runtime.
|
|
// Products provide artifact, runtime, process, and credential ports and expose
|
|
// immutable route projections through their native CLI and Agent runtimes.
|
|
package implementationhost
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"runtime"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
agentruntime "github.com/tutti-os/tutti/packages/agent/daemon/runtime"
|
|
market "github.com/tutti-os/tutti/packages/connector/daemon/core"
|
|
connectorruntime "github.com/tutti-os/tutti/packages/connector/runtime"
|
|
connectorartifact "github.com/tutti-os/tutti/packages/connector/runtime/artifact"
|
|
"github.com/tutti-os/tutti/packages/connector/runtime/command"
|
|
"github.com/tutti-os/tutti/packages/connector/runtime/mcp"
|
|
)
|
|
|
|
type PreparedArtifactResolver interface {
|
|
ResolvePrepared(context.Context, market.Release) (market.PreparedArtifactReceipt, error)
|
|
}
|
|
|
|
type ReconcileRequest struct {
|
|
Runtime market.RuntimeReconcileRequest
|
|
}
|
|
|
|
// RemoteMCPClient is the protocol client required by one remote Connector
|
|
// route. Products decide whether the client connects directly to their MCP
|
|
// Gateway or through a product-owned relay.
|
|
type RemoteMCPClient interface {
|
|
Call(context.Context, string, any) (json.RawMessage, error)
|
|
RegisterTool(string, map[string]any) error
|
|
ReplaceTools(map[string]map[string]any) error
|
|
Close(context.Context) error
|
|
}
|
|
|
|
// RemoteMCPClientRequest carries the complete non-credential identity of one
|
|
// remote Connector route. A product adapter may use it to bind a relay request
|
|
// to the lifecycle operation without exposing account credentials to the
|
|
// runtime machine.
|
|
type RemoteMCPClientRequest struct {
|
|
OperationID string
|
|
ConnectionID string
|
|
ConnectorKey string
|
|
AccountID string
|
|
ReleaseDigest string
|
|
Version string
|
|
Generation market.HostGeneration
|
|
Implementation market.RemoteStreamableHTTPImplementation
|
|
}
|
|
|
|
// RemoteMCPClientFactory is the product port for remote Connector MCP
|
|
// connectivity. ImplementationHost owns protocol bootstrap and route
|
|
// lifecycle; the factory owns the physical connection path and request
|
|
// authorization.
|
|
type RemoteMCPClientFactory interface {
|
|
NewRemoteMCPClient(context.Context, RemoteMCPClientRequest) (RemoteMCPClient, error)
|
|
}
|
|
|
|
type Config struct {
|
|
Artifacts PreparedArtifactResolver
|
|
CLIInstallations market.CLIInstallationManager
|
|
Runtimes connectorruntime.ConnectorRuntimeResolver
|
|
Processes agentruntime.ProcessTransport
|
|
Routes RouteObserver
|
|
Authorization AuthorizationObserver
|
|
Registry *RouteRegistry
|
|
MCP *MCPRegistry
|
|
StateRoot string
|
|
BinDir string
|
|
UserHome string
|
|
MCPStartupTimeout time.Duration
|
|
RemoteMCPClientFactory RemoteMCPClientFactory
|
|
}
|
|
|
|
type Host struct {
|
|
admission sync.RWMutex
|
|
laneMu sync.Mutex
|
|
connectorLanes map[string]*sync.Mutex
|
|
artifacts PreparedArtifactResolver
|
|
planner *connectorruntime.ManagedRoutePlanner
|
|
processes agentruntime.ProcessTransport
|
|
routeObserver RouteObserver
|
|
authorizationObserver AuthorizationObserver
|
|
mcpStartupTimeout time.Duration
|
|
routes *connectorruntime.RouteTable
|
|
snapshots *connectorruntime.ExecutionSnapshotter
|
|
authorizationProvider *managedCredentialAuthorizationProvider
|
|
authorizationMu sync.Mutex
|
|
authorizationRoutes map[string]*connectorRoute
|
|
remoteMCPClientFactory RemoteMCPClientFactory
|
|
mcpRegistry *MCPRegistry
|
|
registry *RouteRegistry
|
|
binDir string
|
|
remoteMCPTools *remoteMCPToolCache
|
|
}
|
|
|
|
type connectorRoute struct {
|
|
id string
|
|
connectionID string
|
|
connectorKey string
|
|
connectorVersion string
|
|
releaseDigest string
|
|
generation market.HostGeneration
|
|
mcpTools map[string]registeredMCPTool
|
|
closeMu sync.Mutex
|
|
mcpClient *mcp.StdioClient
|
|
remoteMCP RemoteMCPClient
|
|
executionRoot string
|
|
installedRoot string
|
|
displayName string
|
|
description string
|
|
routingAliases []string
|
|
skillRoot string
|
|
skills []connectorartifact.SkillSummary
|
|
processes *connectorruntime.ProcessGroup
|
|
snapshots *connectorruntime.ExecutionSnapshotter
|
|
userHome string
|
|
cliLaunch *managedCLILaunch
|
|
cliCommand string
|
|
cliInvocationCommand string
|
|
cliContractHash string
|
|
cliCommands []market.CLICommand
|
|
cliShimPath string
|
|
cliShimContent []byte
|
|
credentialBrokerLaunch *managedCredentialBrokerLaunch
|
|
readiness market.RuntimeReadiness
|
|
}
|
|
|
|
func New(config Config) (*Host, error) {
|
|
if config.Artifacts == nil || config.Runtimes == nil || config.Processes == nil || config.Registry == nil || config.MCP == nil {
|
|
return nil, errors.New("connector implementation host dependencies are required")
|
|
}
|
|
if !filepath.IsAbs(strings.TrimSpace(config.StateRoot)) {
|
|
return nil, errors.New("connector implementation state root must be absolute")
|
|
}
|
|
if !filepath.IsAbs(strings.TrimSpace(config.UserHome)) {
|
|
return nil, errors.New("connector implementation user home must be absolute")
|
|
}
|
|
if strings.TrimSpace(config.BinDir) == "" {
|
|
config.BinDir = filepath.Join(config.StateRoot, "bin")
|
|
}
|
|
if !filepath.IsAbs(config.BinDir) {
|
|
return nil, errors.New("connector CLI bin directory must be absolute")
|
|
}
|
|
if config.MCPStartupTimeout <= 0 {
|
|
config.MCPStartupTimeout = 15 * time.Second
|
|
}
|
|
snapshots, err := connectorruntime.NewExecutionSnapshotter(config.StateRoot)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := snapshots.CleanupOrphans(); err != nil {
|
|
return nil, fmt.Errorf("clean orphaned connector execution snapshots: %w", err)
|
|
}
|
|
routes := connectorruntime.NewRouteTable()
|
|
planner, err := connectorruntime.NewManagedRoutePlanner(connectorruntime.ManagedRoutePlannerConfig{
|
|
StateRoot: config.StateRoot, UserHome: config.UserHome, Runtimes: config.Runtimes, CLIInstallations: config.CLIInstallations,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
config.Registry.attach(routes)
|
|
config.MCP.attach(routes)
|
|
host := &Host{
|
|
artifacts: config.Artifacts,
|
|
planner: planner,
|
|
processes: config.Processes,
|
|
routeObserver: config.Routes,
|
|
authorizationObserver: config.Authorization,
|
|
mcpStartupTimeout: config.MCPStartupTimeout,
|
|
routes: routes,
|
|
snapshots: snapshots,
|
|
authorizationRoutes: make(map[string]*connectorRoute),
|
|
connectorLanes: make(map[string]*sync.Mutex),
|
|
remoteMCPClientFactory: config.RemoteMCPClientFactory,
|
|
mcpRegistry: config.MCP,
|
|
registry: config.Registry,
|
|
binDir: config.BinDir,
|
|
remoteMCPTools: newRemoteMCPToolCache(),
|
|
}
|
|
host.authorizationProvider = newManagedCredentialAuthorizationProvider(host)
|
|
return host, nil
|
|
}
|
|
|
|
func (host *Host) Reconcile(ctx context.Context, request ReconcileRequest) (market.RuntimeReceipt, error) {
|
|
runtimeRequest := request.Runtime
|
|
if host == nil || !hostIdentityPattern.MatchString(runtimeRequest.ConnectionID) ||
|
|
!hostIdentityPattern.MatchString(runtimeRequest.Connector.Key) || runtimeRequest.Generation.BootEpoch == "" || runtimeRequest.Generation.Generation == 0 {
|
|
return market.RuntimeReceipt{}, errors.New("connector runtime reconcile identity is invalid")
|
|
}
|
|
releaseLane := host.enterConnectorLane(runtimeRequest.Connector.Key)
|
|
defer releaseLane()
|
|
if !runtimeRequest.Enabled {
|
|
// A connection id can rotate while an older account route is still
|
|
// present. Disabled is connector-level desired state, so convergence must
|
|
// fence every route and authorization session for the connector instead
|
|
// of removing only the latest route key.
|
|
if err := host.deactivateConnector(market.RuntimeDeactivationRequest{
|
|
ConnectionID: runtimeRequest.ConnectionID,
|
|
ConnectorKey: runtimeRequest.Connector.Key,
|
|
AllConnections: true,
|
|
Generation: runtimeRequest.Generation,
|
|
Deadline: time.Now().Add(3 * time.Second),
|
|
}); err != nil {
|
|
return market.RuntimeReceipt{}, err
|
|
}
|
|
return market.RuntimeReceipt{OperationID: runtimeRequest.OperationID, ConnectionID: runtimeRequest.ConnectionID,
|
|
ConnectorKey: runtimeRequest.Connector.Key, ReleaseDigest: runtimeRequest.Connector.Release.ReleaseDigest,
|
|
Generation: runtimeRequest.Generation,
|
|
Readiness: market.RuntimeReadiness{State: market.RuntimeReadinessBlocked,
|
|
ReasonCode: market.RuntimeReadinessReasonRuntimeDisabled}}, nil
|
|
}
|
|
if err := market.ValidateRuntimeReleaseShape(runtimeRequest.Connector.Release); err != nil {
|
|
return market.RuntimeReceipt{}, err
|
|
}
|
|
key := connectorRouteKey(runtimeRequest.ConnectionID, runtimeRequest.Connector.Key)
|
|
if !installationTargetsRelease(runtimeRequest.Connector.Installation, runtimeRequest.Connector.Release.ReleaseDigest) {
|
|
return market.RuntimeReceipt{}, errors.New("connector installed release is not active")
|
|
}
|
|
if err := host.validateAuthorization(runtimeRequest); err != nil {
|
|
return market.RuntimeReceipt{}, err
|
|
}
|
|
var route *connectorRoute
|
|
var err error
|
|
if runtimeRequest.Connector.Release.Manifest.Implementation.Kind == market.ImplementationKindRemoteStreamableHTTP {
|
|
route, err = host.buildRemoteRoute(ctx, runtimeRequest)
|
|
if err == nil {
|
|
prepared, resolveErr := host.artifacts.ResolvePrepared(ctx, runtimeRequest.Connector.Release)
|
|
if resolveErr != nil {
|
|
_ = route.Close(time.Now().Add(3 * time.Second))
|
|
return market.RuntimeReceipt{}, fmt.Errorf("resolve remote connector artifact: %w", resolveErr)
|
|
}
|
|
route.installedRoot = prepared.PreparedPath
|
|
}
|
|
} else {
|
|
prepared, resolveErr := host.artifacts.ResolvePrepared(ctx, runtimeRequest.Connector.Release)
|
|
if resolveErr != nil {
|
|
return market.RuntimeReceipt{}, fmt.Errorf("resolve prepared connector artifact: %w", resolveErr)
|
|
}
|
|
installedRoot := prepared.PreparedPath
|
|
executionRoot, snapshotErr := host.snapshots.Create(prepared, artifactNativeEntrypoints(runtimeRequest.Connector.Release)...)
|
|
if snapshotErr != nil {
|
|
return market.RuntimeReceipt{}, fmt.Errorf("create connector execution snapshot: %w", snapshotErr)
|
|
}
|
|
prepared.PreparedPath = executionRoot
|
|
route, err = host.buildManagedRoute(ctx, runtimeRequest, prepared)
|
|
if err != nil {
|
|
_ = host.snapshots.Remove(executionRoot)
|
|
return market.RuntimeReceipt{}, err
|
|
}
|
|
route.executionRoot, route.installedRoot = executionRoot, installedRoot
|
|
}
|
|
if err != nil {
|
|
return market.RuntimeReceipt{}, wrapRemoteMCPAuthorizationError(err)
|
|
}
|
|
route.displayName = runtimeRequest.Connector.Release.Manifest.DisplayName
|
|
route.description = runtimeRequest.Connector.Release.Manifest.Description
|
|
if routing := runtimeRequest.Connector.Release.Manifest.AgentRouting; routing != nil {
|
|
route.routingAliases = append([]string(nil), routing.Aliases...)
|
|
}
|
|
route.snapshots = host.snapshots
|
|
skillProjection, err := connectorartifact.InspectSkills(route.installedRoot)
|
|
if err != nil {
|
|
_ = route.Close(time.Now().Add(3 * time.Second))
|
|
return market.RuntimeReceipt{}, fmt.Errorf("inspect connector Skills: %w", err)
|
|
}
|
|
route.skillRoot = skillProjection.Root
|
|
route.skills = append([]connectorartifact.SkillSummary(nil), skillProjection.Skills...)
|
|
if route.readiness.State == "" {
|
|
route.readiness = readyRuntimeReadiness(route)
|
|
}
|
|
previous, _ := host.routes.Route(key).(*connectorRoute)
|
|
if err := route.activateCLIShim(); err != nil {
|
|
_ = route.Close(time.Now().Add(3 * time.Second))
|
|
return market.RuntimeReceipt{}, err
|
|
}
|
|
if err := host.routes.Commit(route); err != nil {
|
|
if previous != nil {
|
|
_ = previous.activateCLIShim()
|
|
} else {
|
|
route.removeCLIShimIfCurrent()
|
|
}
|
|
_ = route.Close(time.Now().Add(3 * time.Second))
|
|
return market.RuntimeReceipt{}, err
|
|
}
|
|
host.releaseAuthorizationRouteByKey(key)
|
|
host.notifyRouteChanged()
|
|
if route.mcpClient != nil {
|
|
go host.monitorMCPRoute(route, route.mcpClient)
|
|
}
|
|
summary := connectorSummaryFromDescriptor(routeDescriptor(route))
|
|
return market.RuntimeReceipt{OperationID: runtimeRequest.OperationID, ConnectionID: runtimeRequest.ConnectionID,
|
|
ConnectorKey: runtimeRequest.Connector.Key, ReleaseDigest: route.releaseDigest,
|
|
Generation: runtimeRequest.Generation, Readiness: cloneRuntimeReadiness(route.readiness), Summary: &summary}, nil
|
|
}
|
|
|
|
func installationTargetsRelease(installation market.Installation, releaseDigest string) bool {
|
|
switch installation.State {
|
|
case market.InstallationStateInstalled:
|
|
return installation.InstalledReleaseDigest == releaseDigest
|
|
case market.InstallationStateInstalling, market.InstallationStateUpdating:
|
|
return installation.CandidateReleaseDigest == releaseDigest
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func (*Host) validateAuthorization(request market.RuntimeReconcileRequest) error {
|
|
authKind := request.Connector.Release.Manifest.AuthorizationKind
|
|
if request.Connector.Release.Manifest.Implementation.Kind == market.ImplementationKindRemoteStreamableHTTP {
|
|
if authKind == "none" {
|
|
if request.Connector.Authorization.State != market.AuthorizationStateNotRequired {
|
|
return errors.New("authorization-free connector has an invalid credential binding")
|
|
}
|
|
return nil
|
|
}
|
|
if request.Connector.Authorization.State != market.AuthorizationStateConnected {
|
|
return market.NewDomainError(
|
|
market.ErrorCodeAuthorizationFailed,
|
|
"authorized remote connector is not connected",
|
|
false,
|
|
nil,
|
|
)
|
|
}
|
|
return nil
|
|
}
|
|
managed := request.Connector.Release.Manifest.Implementation.ManagedStdio
|
|
if authKind == "none" {
|
|
if request.Connector.Authorization.State != market.AuthorizationStateNotRequired {
|
|
return errors.New("authorization-free connector has an invalid credential binding")
|
|
}
|
|
return nil
|
|
}
|
|
if managed == nil || managed.CredentialBroker == nil {
|
|
return errors.New("authorized connector credential broker binding is unavailable")
|
|
}
|
|
if request.Connector.Authorization.State != market.AuthorizationStateConnected {
|
|
return market.NewDomainError(
|
|
market.ErrorCodeAuthorizationFailed,
|
|
"authorized managed connector is not connected",
|
|
false,
|
|
nil,
|
|
)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func newConnectorRoute(request market.RuntimeReconcileRequest) *connectorRoute {
|
|
return &connectorRoute{id: connectorRouteKey(request.ConnectionID, request.Connector.Key), connectionID: request.ConnectionID,
|
|
connectorKey: request.Connector.Key, connectorVersion: request.Connector.Release.Version,
|
|
releaseDigest: request.Connector.Release.ReleaseDigest,
|
|
generation: request.Generation, mcpTools: make(map[string]registeredMCPTool),
|
|
processes: connectorruntime.NewProcessGroup()}
|
|
}
|
|
|
|
func (host *Host) Close() error {
|
|
if host == nil {
|
|
return nil
|
|
}
|
|
deadline := time.Now().Add(3 * time.Second)
|
|
host.admission.Lock()
|
|
defer host.admission.Unlock()
|
|
host.authorizationMu.Lock()
|
|
authorizationRoutes := make([]*connectorRoute, 0, len(host.authorizationRoutes))
|
|
for key, route := range host.authorizationRoutes {
|
|
delete(host.authorizationRoutes, key)
|
|
authorizationRoutes = append(authorizationRoutes, route)
|
|
}
|
|
host.authorizationMu.Unlock()
|
|
errs := []error{host.routes.Close(deadline)}
|
|
for _, route := range authorizationRoutes {
|
|
route.Fence()
|
|
errs = append(errs, route.Close(deadline))
|
|
}
|
|
host.remoteMCPTools.clear()
|
|
return errors.Join(errs...)
|
|
}
|
|
|
|
func (host *Host) SetCapabilityPublication(enabled bool) {
|
|
if host != nil {
|
|
host.routes.SetPublished(enabled)
|
|
host.notifyRouteChanged()
|
|
}
|
|
}
|
|
|
|
func (host *Host) FenceAll(_ context.Context, deadline time.Time) error {
|
|
if host == nil {
|
|
return nil
|
|
}
|
|
host.admission.Lock()
|
|
defer host.admission.Unlock()
|
|
err := host.routes.FenceAll(deadline)
|
|
host.notifyRouteChanged()
|
|
return err
|
|
}
|
|
|
|
func (host *Host) FailClosed(ctx context.Context, deadline time.Time) error {
|
|
if host == nil {
|
|
return nil
|
|
}
|
|
host.SetCapabilityPublication(false)
|
|
return host.FenceAll(ctx, deadline)
|
|
}
|
|
|
|
func (host *Host) DeactivateRuntime(ctx context.Context, request market.RuntimeDeactivationRequest) error {
|
|
if host == nil {
|
|
return errors.New("connector implementation host is unavailable")
|
|
}
|
|
if !request.Deadline.IsZero() && time.Now().After(request.Deadline) {
|
|
return context.DeadlineExceeded
|
|
}
|
|
if err := ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
releaseLane := host.enterConnectorLane(request.ConnectorKey)
|
|
defer releaseLane()
|
|
if request.AllConnections {
|
|
return host.deactivateConnector(request)
|
|
}
|
|
err := host.routes.Remove(connectorRouteKey(request.ConnectionID, request.ConnectorKey), request.Generation, request.ReleaseDigest, request.Deadline)
|
|
host.notifyRouteChanged()
|
|
return err
|
|
}
|
|
|
|
func (host *Host) enterConnectorLane(connectorKey string) func() {
|
|
host.admission.RLock()
|
|
host.laneMu.Lock()
|
|
if host.connectorLanes == nil {
|
|
host.connectorLanes = make(map[string]*sync.Mutex)
|
|
}
|
|
lane := host.connectorLanes[connectorKey]
|
|
if lane == nil {
|
|
lane = &sync.Mutex{}
|
|
host.connectorLanes[connectorKey] = lane
|
|
}
|
|
host.laneMu.Unlock()
|
|
lane.Lock()
|
|
return func() {
|
|
lane.Unlock()
|
|
host.admission.RUnlock()
|
|
}
|
|
}
|
|
|
|
func (host *Host) buildManagedRoute(ctx context.Context, request market.RuntimeReconcileRequest,
|
|
prepared market.PreparedArtifactReceipt) (*connectorRoute, error) {
|
|
plan, err := host.planner.Build(ctx, request, prepared)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
route := newConnectorRoute(request)
|
|
route.userHome = plan.UserHome
|
|
if plan.Managed.MCP != nil {
|
|
if err := host.attachMCP(ctx, route, plan.Managed, prepared, plan.Executable, plan.StateDir, plan.UserHome, plan.ArtifactTrees); err != nil {
|
|
_ = route.close(time.Now().Add(3 * time.Second))
|
|
return nil, err
|
|
}
|
|
}
|
|
if plan.Managed.CLI != nil {
|
|
if err := host.attachCLI(route, plan.Managed, prepared, plan.InstalledCLI, plan.Executable, plan.StateDir, plan.ArtifactTrees); err != nil {
|
|
_ = route.close(time.Now().Add(3 * time.Second))
|
|
return nil, err
|
|
}
|
|
if err := route.prepareCLIShim(host.binDir); err != nil {
|
|
_ = route.close(time.Now().Add(3 * time.Second))
|
|
return nil, err
|
|
}
|
|
if err := host.checkCLIReadiness(ctx, route, plan.Managed.CLI.ReadinessProbe); err != nil {
|
|
_ = route.close(time.Now().Add(3 * time.Second))
|
|
return nil, fmt.Errorf("check connector CLI readiness: %w", err)
|
|
}
|
|
}
|
|
if plan.Managed.CredentialBroker != nil {
|
|
if err := host.attachCredentialBroker(route, plan.Managed.CredentialBroker, prepared, plan.Executable, plan.StateDir, plan.ArtifactTrees); err != nil {
|
|
_ = route.close(time.Now().Add(3 * time.Second))
|
|
return nil, err
|
|
}
|
|
}
|
|
if route.cliLaunch == nil && len(route.mcpTools) == 0 {
|
|
_ = route.close(time.Now().Add(3 * time.Second))
|
|
return nil, errors.New("connector implementation exposed no MCP tools or CLI commands")
|
|
}
|
|
route.readiness = readyRuntimeReadiness(route)
|
|
return route, nil
|
|
}
|
|
|
|
var mcpLocalToolNamePattern = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9_.:-]*$`)
|
|
var hostIdentityPattern = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._-]{0,190}$`)
|
|
|
|
func (*Host) attachCredentialBroker(route *connectorRoute, broker *market.ManagedCredentialBroker,
|
|
prepared market.PreparedArtifactReceipt, executable connectorruntime.ConnectorExecutable,
|
|
stateDir string, artifactTrees []agentruntime.ArtifactTreeIdentity) error {
|
|
if route.cliLaunch == nil {
|
|
return errors.New("connector credential broker requires a managed CLI")
|
|
}
|
|
entrypoint, err := connectorruntime.PreparedEntrypoint(prepared.PreparedPath, broker.Entrypoint)
|
|
if err != nil {
|
|
return fmt.Errorf("resolve connector credential broker entrypoint: %w", err)
|
|
}
|
|
allowedHosts := make(map[string]struct{}, len(broker.AllowedHosts))
|
|
for _, allowedHost := range broker.AllowedHosts {
|
|
allowedHosts[strings.ToLower(strings.TrimSpace(allowedHost))] = struct{}{}
|
|
}
|
|
route.credentialBrokerLaunch = &managedCredentialBrokerLaunch{
|
|
entrypoint: entrypoint, timeout: time.Duration(broker.TimeoutMS) * time.Millisecond, allowedHosts: allowedHosts,
|
|
cliLaunch: credentialBrokerCLILaunch{Executable: route.cliLaunch.executable.Path,
|
|
// Native CLIs legitimately have no argv; preserve [] instead of encoding null for the broker protocol.
|
|
Arguments: append([]string{}, route.cliLaunch.arguments...), CWD: route.cliLaunch.cwd},
|
|
executable: executable, language: route.cliLaunch.language, cwd: prepared.PreparedPath, stateDir: stateDir,
|
|
artifactTrees: append([]agentruntime.ArtifactTreeIdentity(nil), artifactTrees...),
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (host *Host) attachMCP(ctx context.Context, route *connectorRoute, managed *market.ManagedStdioImplementation,
|
|
prepared market.PreparedArtifactReceipt, executable connectorruntime.ConnectorExecutable,
|
|
stateDir, userHome string, artifactTrees []agentruntime.ArtifactTreeIdentity) error {
|
|
entrypoint, err := connectorruntime.PreparedEntrypoint(prepared.PreparedPath, managed.MCP.Entrypoint)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
startupContext, cancelStartup := context.WithTimeout(ctx, host.mcpStartupTimeout)
|
|
defer cancelStartup()
|
|
spec := connectorruntime.ConnectorProcessSpec(route.connectionID, route.connectorKey, managed.Runtime.Language,
|
|
executable, prepared.PreparedPath, append([]string{entrypoint}, managed.MCP.Arguments...), stateDir, userHome, artifactTrees)
|
|
connection, processID, err := host.startProcess(startupContext, route, spec, false)
|
|
if err != nil {
|
|
return fmt.Errorf("start connector MCP process: %w", err)
|
|
}
|
|
release := func() { _ = route.releaseProcess(processID, connection) }
|
|
client, err := mcp.NewStdioClient(mcp.StdioClientConfig{Connection: connection, ProcessName: route.connectorKey + " MCP"})
|
|
if err != nil {
|
|
release()
|
|
return err
|
|
}
|
|
if _, err := client.Call(startupContext, "initialize", map[string]any{"protocolVersion": "2025-06-18", "capabilities": map[string]any{},
|
|
"clientInfo": map[string]any{"name": "tutti-connector-host", "version": "1"}}); err != nil {
|
|
release()
|
|
return fmt.Errorf("initialize connector MCP process: %w", err)
|
|
}
|
|
if err := client.Notify("notifications/initialized", map[string]any{}); err != nil {
|
|
release()
|
|
return err
|
|
}
|
|
tools, err := listMCPTools(startupContext, client)
|
|
if err != nil {
|
|
release()
|
|
return err
|
|
}
|
|
if len(tools) == 0 {
|
|
release()
|
|
return errors.New("connector MCP tools/list response is invalid")
|
|
}
|
|
if err := host.registerMCPTools(route, client, tools); err != nil {
|
|
release()
|
|
return err
|
|
}
|
|
route.mcpClient = client
|
|
return nil
|
|
}
|
|
|
|
func readyRuntimeReadiness(route *connectorRoute) market.RuntimeReadiness {
|
|
interfaces := make([]market.InterfaceReadiness, 0, 2)
|
|
if len(route.mcpTools) > 0 {
|
|
routeIDs := make([]string, 0, len(route.mcpTools))
|
|
for _, tool := range route.mcpTools {
|
|
routeIDs = append(routeIDs, tool.routeID)
|
|
}
|
|
sort.Strings(routeIDs)
|
|
interfaces = append(interfaces, market.InterfaceReadiness{Kind: "mcp", State: market.RuntimeReadinessReady, RouteIDs: routeIDs})
|
|
}
|
|
if route.cliLaunch != nil {
|
|
interfaces = append(interfaces, market.InterfaceReadiness{Kind: "cli", State: market.RuntimeReadinessReady})
|
|
}
|
|
return market.RuntimeReadiness{State: market.RuntimeReadinessReady, Interfaces: interfaces}
|
|
}
|
|
|
|
func cloneRuntimeReadiness(readiness market.RuntimeReadiness) market.RuntimeReadiness {
|
|
cloned := readiness
|
|
cloned.Interfaces = append([]market.InterfaceReadiness(nil), readiness.Interfaces...)
|
|
for index := range cloned.Interfaces {
|
|
cloned.Interfaces[index].RouteIDs = append([]string(nil), readiness.Interfaces[index].RouteIDs...)
|
|
}
|
|
return cloned
|
|
}
|
|
|
|
func (host *Host) notifyRouteChanged() {
|
|
if host == nil {
|
|
return
|
|
}
|
|
host.registry.notifyChanged()
|
|
host.mcpRegistry.notifyChanged()
|
|
}
|
|
|
|
func (host *Host) monitorMCPRoute(route *connectorRoute, client *mcp.StdioClient) {
|
|
<-client.Done()
|
|
unexpected := host.routes.IsCurrent(route)
|
|
_ = host.routes.RetireExact(route, time.Now().Add(3*time.Second))
|
|
host.notifyRouteChanged()
|
|
if unexpected && host.routeObserver != nil {
|
|
host.routeObserver.ObserveRoute(context.Background(), RouteObservation{
|
|
ConnectorKey: route.connectorKey, ConnectionID: route.connectionID,
|
|
ReleaseDigest: route.releaseDigest, Generation: route.generation, ObservedAt: time.Now().UTC(),
|
|
})
|
|
}
|
|
}
|
|
|
|
func (*Host) attachCLI(route *connectorRoute, managed *market.ManagedStdioImplementation,
|
|
prepared market.PreparedArtifactReceipt, installed *market.CLIInstallationReceipt,
|
|
executable connectorruntime.ConnectorExecutable, stateDir string, artifactTrees []agentruntime.ArtifactTreeIdentity) error {
|
|
entrypointRoot, entrypointRelative := prepared.PreparedPath, managed.CLI.Entrypoint
|
|
if installed != nil {
|
|
entrypointRoot, entrypointRelative = installed.InstallRoot, installed.Entrypoint
|
|
}
|
|
entrypoint, err := connectorruntime.PreparedEntrypoint(entrypointRoot, entrypointRelative)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
contractHash, err := market.ManagedCLIContractHash(*managed.CLI)
|
|
if err != nil {
|
|
return fmt.Errorf("hash connector CLI contract: %w", err)
|
|
}
|
|
launchArguments := []string{entrypoint}
|
|
launchExecutable := executable
|
|
if managed.CLI.Launch != nil && managed.CLI.Launch.Kind == market.CLIArtifactLaunchKindNative {
|
|
launchArguments = nil
|
|
launchExecutable = connectorruntime.ConnectorExecutable{Path: entrypoint, SHA256: managed.CLI.Launch.SHA256,
|
|
SizeBytes: managed.CLI.Launch.SizeBytes}
|
|
} else if installed != nil && installed.LaunchKind == "native" {
|
|
launchArguments = nil
|
|
launchExecutable = connectorruntime.ConnectorExecutable{Path: entrypoint, SHA256: installed.EntrypointSHA256,
|
|
SizeBytes: installed.EntrypointSize}
|
|
}
|
|
route.cliLaunch = &managedCLILaunch{arguments: append(append([]string{}, launchArguments...), managed.CLI.Arguments...),
|
|
artifactTrees: append([]agentruntime.ArtifactTreeIdentity(nil), artifactTrees...), cwd: prepared.PreparedPath,
|
|
executable: launchExecutable, language: managed.Runtime.Language, stateDir: stateDir,
|
|
timeout: time.Duration(managed.CLI.TimeoutMS) * time.Millisecond}
|
|
route.cliContractHash = contractHash
|
|
route.cliInvocationCommand = market.ManagedCLICommandName(*managed.CLI)
|
|
route.cliCommands = cloneCLICommands(managed.CLI.Commands)
|
|
return nil
|
|
}
|
|
|
|
func artifactNativeEntrypoints(release market.Release) []string {
|
|
managed := release.Manifest.Implementation.ManagedStdio
|
|
if managed == nil || managed.CLI == nil || managed.CLI.Launch == nil ||
|
|
managed.CLI.Launch.Kind != market.CLIArtifactLaunchKindNative {
|
|
return nil
|
|
}
|
|
return []string{managed.CLI.Entrypoint}
|
|
}
|
|
|
|
func (host *Host) startProcess(ctx context.Context, route *connectorRoute, spec agentruntime.ProcessSpec,
|
|
requireCurrent bool) (agentruntime.ProcessConnection, uint64, error) {
|
|
if requireCurrent && !host.routes.IsCurrent(route) {
|
|
return nil, 0, command.ErrServiceUnavailable
|
|
}
|
|
startContext, processID, accepted := route.processes.Begin(context.Background())
|
|
if !accepted {
|
|
return nil, 0, command.ErrServiceUnavailable
|
|
}
|
|
type startResult struct {
|
|
connection agentruntime.ProcessConnection
|
|
err error
|
|
}
|
|
result := make(chan startResult, 1)
|
|
go func() {
|
|
connection, startErr := host.processes.Start(startContext, spec)
|
|
result <- startResult{connection: connection, err: startErr}
|
|
}()
|
|
select {
|
|
case started := <-result:
|
|
if started.err != nil {
|
|
route.processes.FailStart(processID)
|
|
return nil, 0, started.err
|
|
}
|
|
if !route.processes.CommitStart(processID, started.connection) {
|
|
_ = started.connection.Close()
|
|
return nil, 0, command.ErrServiceUnavailable
|
|
}
|
|
return started.connection, processID, nil
|
|
case <-ctx.Done():
|
|
route.processes.FailStart(processID)
|
|
go func() {
|
|
started := <-result
|
|
if started.connection != nil {
|
|
_ = started.connection.Close()
|
|
}
|
|
}()
|
|
return nil, 0, ctx.Err()
|
|
}
|
|
}
|
|
|
|
func connectorRouteKey(connectionID, connectorKey string) string {
|
|
return connectionID + "\x00" + connectorKey
|
|
}
|
|
|
|
func (host *Host) routeCurrent(route *connectorRoute) bool {
|
|
return host.routes.IsCurrent(route) && !route.processes.IsFenced()
|
|
}
|
|
|
|
func (route *connectorRoute) RouteID() string { return route.id }
|
|
func (route *connectorRoute) RouteGeneration() market.HostGeneration { return route.generation }
|
|
func (route *connectorRoute) RouteReleaseDigest() string { return route.releaseDigest }
|
|
func (route *connectorRoute) Fence() { route.processes.Fence() }
|
|
func (route *connectorRoute) close(deadline time.Time) error { return route.Close(deadline) }
|
|
func (route *connectorRoute) releaseProcess(id uint64, connection agentruntime.ProcessConnection) error {
|
|
if route != nil && route.processes != nil {
|
|
return route.processes.ReleaseWithError(id, connection)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (route *connectorRoute) Close(deadline time.Time) error {
|
|
if route == nil {
|
|
return nil
|
|
}
|
|
route.closeMu.Lock()
|
|
defer route.closeMu.Unlock()
|
|
route.removeCLIShimIfCurrent()
|
|
var remoteErr error
|
|
if route.remoteMCP != nil {
|
|
closeCtx, cancel := context.WithDeadline(context.Background(), deadline)
|
|
remoteErr = route.remoteMCP.Close(closeCtx)
|
|
cancel()
|
|
}
|
|
closeErr := route.processes.Close(deadline)
|
|
if closeErr == nil {
|
|
closeErr = remoteErr
|
|
}
|
|
if closeErr == nil && route.executionRoot != "" {
|
|
if err := route.snapshots.Remove(route.executionRoot); err != nil {
|
|
closeErr = err
|
|
} else {
|
|
route.executionRoot = ""
|
|
}
|
|
}
|
|
return closeErr
|
|
}
|
|
|
|
func (route *connectorRoute) prepareCLIShim(binDir string) error {
|
|
if route == nil || route.cliLaunch == nil {
|
|
return nil
|
|
}
|
|
command := "tutti-connector-" + route.connectorKey
|
|
if runtime.GOOS == "windows" {
|
|
command += ".cmd"
|
|
}
|
|
path := filepath.Join(binDir, command)
|
|
content, err := connectorCLIShimContent(route)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
route.cliCommand, route.cliShimPath, route.cliShimContent = command, path, content
|
|
return nil
|
|
}
|
|
|
|
func (route *connectorRoute) activateCLIShim() error {
|
|
if route == nil || route.cliShimPath == "" || len(route.cliShimContent) == 0 {
|
|
return nil
|
|
}
|
|
if err := os.MkdirAll(filepath.Dir(route.cliShimPath), 0o700); err != nil {
|
|
return fmt.Errorf("create connector CLI bin directory: %w", err)
|
|
}
|
|
temporary := route.cliShimPath + ".tmp-" + fmt.Sprintf("%d", route.generation.Generation)
|
|
if err := os.WriteFile(temporary, route.cliShimContent, 0o700); err != nil {
|
|
return fmt.Errorf("write connector CLI shim: %w", err)
|
|
}
|
|
if err := os.Rename(temporary, route.cliShimPath); err != nil {
|
|
_ = os.Remove(temporary)
|
|
return fmt.Errorf("activate connector CLI shim: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (route *connectorRoute) removeCLIShimIfCurrent() {
|
|
if route == nil || route.cliShimPath == "" {
|
|
return
|
|
}
|
|
current, err := os.ReadFile(route.cliShimPath)
|
|
if err == nil && string(current) == string(route.cliShimContent) {
|
|
_ = os.Remove(route.cliShimPath)
|
|
}
|
|
}
|
|
|
|
func connectorCLIShimContent(route *connectorRoute) ([]byte, error) {
|
|
launch := route.cliLaunch
|
|
if launch == nil || strings.TrimSpace(launch.executable.Path) == "" {
|
|
return nil, errors.New("connector CLI launch is unavailable")
|
|
}
|
|
arguments := append([]string(nil), launch.arguments...)
|
|
if runtime.GOOS == "windows" {
|
|
values := append([]string{launch.executable.Path}, arguments...)
|
|
for _, value := range append(values, launch.cwd, launch.stateDir, route.userHome) {
|
|
if strings.ContainsAny(value, "\r\n\"") {
|
|
return nil, errors.New("connector CLI path cannot be represented by Windows shim")
|
|
}
|
|
}
|
|
quoted := make([]string, 0, len(values))
|
|
for _, value := range values {
|
|
quoted = append(quoted, `"`+value+`"`)
|
|
}
|
|
content := "@echo off\r\n" +
|
|
"set \"TUTTI_CONNECTOR_CONNECTION_ID=" + route.connectionID + "\"\r\n" +
|
|
"set \"TUTTI_CONNECTOR_KEY=" + route.connectorKey + "\"\r\n" +
|
|
"set \"TUTTI_CONNECTOR_LANGUAGE=" + launch.language + "\"\r\n" +
|
|
"set \"TUTTI_CONNECTOR_STATE_DIR=" + launch.stateDir + "\"\r\n" +
|
|
"set \"HOME=" + route.userHome + "\"\r\n" +
|
|
"set \"USERPROFILE=" + route.userHome + "\"\r\n" +
|
|
"cd /d \"" + launch.cwd + "\"\r\n" + strings.Join(quoted, " ") + " %*\r\n"
|
|
return []byte(content), nil
|
|
}
|
|
values := append([]string{launch.executable.Path}, arguments...)
|
|
quoted := make([]string, 0, len(values))
|
|
for _, value := range values {
|
|
quoted = append(quoted, shellQuote(value))
|
|
}
|
|
content := "#!/bin/sh\n" +
|
|
"export TUTTI_CONNECTOR_CONNECTION_ID=" + shellQuote(route.connectionID) + "\n" +
|
|
"export TUTTI_CONNECTOR_KEY=" + shellQuote(route.connectorKey) + "\n" +
|
|
"export TUTTI_CONNECTOR_LANGUAGE=" + shellQuote(launch.language) + "\n" +
|
|
"export TUTTI_CONNECTOR_STATE_DIR=" + shellQuote(launch.stateDir) + "\n" +
|
|
"export HOME=" + shellQuote(route.userHome) + "\n" +
|
|
"export USERPROFILE=" + shellQuote(route.userHome) + "\n" +
|
|
"cd " + shellQuote(launch.cwd) + " || exit 1\n" +
|
|
"exec " + strings.Join(quoted, " ") + " \"$@\"\n"
|
|
return []byte(content), nil
|
|
}
|
|
|
|
func shellQuote(value string) string {
|
|
return "'" + strings.ReplaceAll(value, "'", "'\"'\"'") + "'"
|
|
}
|
|
|
|
var _ connectorruntime.ManagedRoute = (*connectorRoute)(nil)
|