源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
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

This commit is contained in:
agent-radar
2026-09-01 20:17:27 +08:00
commit 2ed3415fb7
8018 changed files with 1936476 additions and 0 deletions
@@ -0,0 +1,201 @@
package implementationhost
import (
"context"
"errors"
"path/filepath"
"regexp"
"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"
)
var cliContractHashPattern = regexp.MustCompile(`^sha256:[a-f0-9]{64}$`)
const (
maxCLIExecutionArguments = 256
maxCLIExecutionArgBytes = 256 << 10
)
var (
ErrCLIExecutionInvalid = errors.New("connector CLI execution request is invalid")
ErrCLIExecutionUnavailable = errors.New("connector CLI execution is unavailable")
ErrCLIExecutionIdentityMismatch = errors.New("connector CLI execution identity does not match the current route")
)
// CLIExecutionRequest binds one raw CLI invocation to the exact immutable
// route selected and authorized by a product-owned Connector broker.
type CLIExecutionRequest struct {
ConnectionID string
ConnectorKey string
ConnectorVersion string
ReleaseDigest string
Generation market.HostGeneration
CLIContractHash string
WorkingDirectory string
Arguments []string
}
// StartCLI launches one current managed CLI route. The caller supplies only
// the per-invocation working directory and argv. The executable, environment,
// state path, user home, and artifact identities remain owned by the exact
// current Connector route.
func (host *Host) StartCLI(ctx context.Context, request CLIExecutionRequest) (agentruntime.ProcessConnection, error) {
if err := validateCLIExecutionRequest(ctx, request); err != nil {
return nil, err
}
if host == nil || host.routes == nil || host.processes == nil {
return nil, ErrCLIExecutionUnavailable
}
route, _ := host.routes.Route(connectorRouteKey(request.ConnectionID, request.ConnectorKey)).(*connectorRoute)
if route == nil || !host.routeCurrent(route) || route.cliLaunch == nil || route.cliContractHash == "" {
return nil, ErrCLIExecutionUnavailable
}
if route.connectorVersion != request.ConnectorVersion || route.releaseDigest != request.ReleaseDigest ||
route.generation != request.Generation || route.cliContractHash != request.CLIContractHash {
return nil, ErrCLIExecutionIdentityMismatch
}
launch := route.cliLaunch
arguments := append(append([]string(nil), launch.arguments...), request.Arguments...)
spec := connectorruntime.ConnectorProcessSpec(route.connectionID, route.connectorKey, launch.language,
launch.executable, request.WorkingDirectory, arguments, launch.stateDir, route.userHome, launch.artifactTrees)
startCtx := ctx
var executionDeadline time.Time
var cancel context.CancelFunc
if launch.timeout > 0 {
executionDeadline = time.Now().Add(launch.timeout)
startCtx, cancel = context.WithDeadline(ctx, executionDeadline)
defer cancel()
}
connection, processID, err := host.startProcess(startCtx, route, spec, true)
if err != nil {
return nil, errors.Join(ErrCLIExecutionUnavailable, err)
}
return wrapCLIExecutionConnection(route, processID, connection, executionDeadline), nil
}
func validateCLIExecutionRequest(ctx context.Context, request CLIExecutionRequest) error {
if ctx == nil || !hostIdentityPattern.MatchString(request.ConnectionID) || !hostIdentityPattern.MatchString(request.ConnectorKey) ||
strings.TrimSpace(request.ConnectorVersion) == "" || strings.TrimSpace(request.ReleaseDigest) == "" ||
request.Generation.BootEpoch == "" || request.Generation.Generation == 0 ||
!cliContractHashPattern.MatchString(request.CLIContractHash) || !filepath.IsAbs(request.WorkingDirectory) ||
strings.ContainsRune(request.WorkingDirectory, '\x00') || len(request.Arguments) > maxCLIExecutionArguments {
return ErrCLIExecutionInvalid
}
totalBytes := 0
for _, argument := range request.Arguments {
totalBytes += len(argument)
if strings.ContainsRune(argument, '\x00') || totalBytes > maxCLIExecutionArgBytes {
return ErrCLIExecutionInvalid
}
}
return nil
}
type cliExecutionConnection struct {
route *connectorRoute
processID uint64
connection agentruntime.ProcessConnection
closeOnce sync.Once
closeErr error
execution context.Context
cancel context.CancelFunc
}
func (connection *cliExecutionConnection) Send(payload []byte) error {
return connection.connection.Send(payload)
}
func (connection *cliExecutionConnection) Recv() (agentruntime.ProcessFrame, error) {
return connection.connection.Recv()
}
func (connection *cliExecutionConnection) Close() error {
connection.closeOnce.Do(func() {
if connection.cancel != nil {
connection.cancel()
}
connection.closeErr = connection.route.releaseProcess(connection.processID, connection.connection)
})
return connection.closeErr
}
type contextualCLIExecutionConnection struct {
*cliExecutionConnection
contextual agentruntime.ContextProcessConnection
}
func (connection *contextualCLIExecutionConnection) RecvContext(ctx context.Context) (agentruntime.ProcessFrame, error) {
merged, cancel := mergeCLIExecutionContext(ctx, connection.execution)
defer cancel()
frame, err := connection.contextual.RecvContext(merged)
if connection.execution != nil && errors.Is(connection.execution.Err(), context.DeadlineExceeded) {
return agentruntime.ProcessFrame{}, context.DeadlineExceeded
}
return frame, err
}
type gracefulCLIExecutionConnection struct {
*cliExecutionConnection
graceful agentruntime.GracefulProcessConnection
}
func (connection *gracefulCLIExecutionConnection) CloseInput() error {
return connection.graceful.CloseInput()
}
func (connection *gracefulCLIExecutionConnection) Terminate() error {
return connection.graceful.Terminate()
}
func (connection *gracefulCLIExecutionConnection) Kill() error { return connection.graceful.Kill() }
type contextualGracefulCLIExecutionConnection struct {
*gracefulCLIExecutionConnection
contextual agentruntime.ContextProcessConnection
}
func (connection *contextualGracefulCLIExecutionConnection) RecvContext(ctx context.Context) (agentruntime.ProcessFrame, error) {
merged, cancel := mergeCLIExecutionContext(ctx, connection.execution)
defer cancel()
frame, err := connection.contextual.RecvContext(merged)
if connection.execution != nil && errors.Is(connection.execution.Err(), context.DeadlineExceeded) {
return agentruntime.ProcessFrame{}, context.DeadlineExceeded
}
return frame, err
}
func wrapCLIExecutionConnection(route *connectorRoute, processID uint64,
connection agentruntime.ProcessConnection, executionDeadline time.Time) agentruntime.ProcessConnection {
base := &cliExecutionConnection{route: route, processID: processID, connection: connection}
if !executionDeadline.IsZero() {
base.execution, base.cancel = context.WithDeadline(context.Background(), executionDeadline)
}
contextual, hasContext := connection.(agentruntime.ContextProcessConnection)
graceful, hasGraceful := connection.(agentruntime.GracefulProcessConnection)
switch {
case hasContext && hasGraceful:
return &contextualGracefulCLIExecutionConnection{
gracefulCLIExecutionConnection: &gracefulCLIExecutionConnection{cliExecutionConnection: base, graceful: graceful},
contextual: contextual,
}
case hasContext:
return &contextualCLIExecutionConnection{cliExecutionConnection: base, contextual: contextual}
case hasGraceful:
return &gracefulCLIExecutionConnection{cliExecutionConnection: base, graceful: graceful}
default:
return base
}
}
func mergeCLIExecutionContext(caller, execution context.Context) (context.Context, context.CancelFunc) {
if execution == nil {
return context.WithCancel(caller)
}
merged, cancel := context.WithCancel(caller)
stop := context.AfterFunc(execution, cancel)
return merged, func() {
stop()
cancel()
}
}
@@ -0,0 +1,305 @@
package implementationhost
import (
"context"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"reflect"
"strings"
"sync"
"testing"
"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"
)
const cliExecutorTestDigest = "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"
const cliExecutorTestContractHash = "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
type cliExecutionTransportStub struct {
mu sync.Mutex
specs []agentruntime.ProcessSpec
connection *cliExecutionConnectionStub
}
func (transport *cliExecutionTransportStub) Start(_ context.Context, spec agentruntime.ProcessSpec) (agentruntime.ProcessConnection, error) {
transport.mu.Lock()
defer transport.mu.Unlock()
transport.specs = append(transport.specs, spec)
transport.connection = &cliExecutionConnectionStub{}
return transport.connection, nil
}
type cliExecutionConnectionStub struct {
mu sync.Mutex
closed int
inputClosed int
terminated int
killed int
recvContextual int
blockRecv bool
}
func (*cliExecutionConnectionStub) Send([]byte) error { return nil }
func (*cliExecutionConnectionStub) Recv() (agentruntime.ProcessFrame, error) {
return agentruntime.ProcessFrame{}, io.EOF
}
func (connection *cliExecutionConnectionStub) RecvContext(ctx context.Context) (agentruntime.ProcessFrame, error) {
connection.mu.Lock()
connection.recvContextual++
block := connection.blockRecv
connection.mu.Unlock()
if block {
<-ctx.Done()
return agentruntime.ProcessFrame{}, ctx.Err()
}
return agentruntime.ProcessFrame{}, io.EOF
}
func (connection *cliExecutionConnectionStub) Close() error {
connection.mu.Lock()
connection.closed++
connection.mu.Unlock()
return nil
}
func (connection *cliExecutionConnectionStub) CloseInput() error {
connection.mu.Lock()
connection.inputClosed++
connection.mu.Unlock()
return nil
}
func (connection *cliExecutionConnectionStub) Terminate() error {
connection.mu.Lock()
connection.terminated++
connection.mu.Unlock()
return nil
}
func (connection *cliExecutionConnectionStub) Kill() error {
connection.mu.Lock()
connection.killed++
connection.mu.Unlock()
return nil
}
func TestStartCLIExecutesExactCurrentRouteAndOwnsLifecycle(t *testing.T) {
host, route, transport, request := newCLIExecutionTestHost(t)
connection, err := host.StartCLI(context.Background(), request)
if err != nil {
t.Fatal(err)
}
if route.processes.ActiveCount() != 1 {
t.Fatalf("active processes = %d, want 1", route.processes.ActiveCount())
}
transport.mu.Lock()
spec := transport.specs[0]
inner := transport.connection
transport.mu.Unlock()
wantCommand := append([]string{route.cliLaunch.executable.Path}, route.cliLaunch.arguments...)
wantCommand = append(wantCommand, request.Arguments...)
if !reflect.DeepEqual(spec.Command, wantCommand) {
t.Fatalf("command = %#v, want %#v", spec.Command, wantCommand)
}
if spec.CWD != request.WorkingDirectory || !reflect.DeepEqual(spec.Env, []string{
"TUTTI_CONNECTOR_CONNECTION_ID=connection-1", "TUTTI_CONNECTOR_KEY=lark-cli", "TUTTI_CONNECTOR_LANGUAGE=node",
"TUTTI_CONNECTOR_STATE_DIR=" + route.cliLaunch.stateDir, "HOME=" + route.userHome, "USERPROFILE=" + route.userHome,
}) {
t.Fatalf("process spec = %#v", spec)
}
if _, ok := connection.(agentruntime.ContextProcessConnection); !ok {
t.Fatal("context process capability was not preserved")
}
graceful, ok := connection.(agentruntime.GracefulProcessConnection)
if !ok {
t.Fatal("graceful process capability was not preserved")
}
if err := graceful.CloseInput(); err != nil {
t.Fatal(err)
}
if err := connection.Close(); err != nil {
t.Fatal(err)
}
if err := connection.Close(); err != nil {
t.Fatal(err)
}
inner.mu.Lock()
defer inner.mu.Unlock()
if inner.closed != 1 || inner.inputClosed != 1 || route.processes.ActiveCount() != 0 {
t.Fatalf("closed=%d inputClosed=%d active=%d", inner.closed, inner.inputClosed, route.processes.ActiveCount())
}
}
func TestStartCLIFailsClosedForStaleOrInvalidIdentity(t *testing.T) {
host, _, transport, request := newCLIExecutionTestHost(t)
stale := request
stale.CLIContractHash = "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
if _, err := host.StartCLI(context.Background(), stale); !errors.Is(err, ErrCLIExecutionIdentityMismatch) {
t.Fatalf("stale identity error = %v", err)
}
invalid := request
invalid.Arguments = []string{"bad\x00argument"}
if _, err := host.StartCLI(context.Background(), invalid); !errors.Is(err, ErrCLIExecutionInvalid) {
t.Fatalf("invalid argument error = %v", err)
}
invalid = request
invalid.WorkingDirectory = "relative/workspace"
if _, err := host.StartCLI(context.Background(), invalid); !errors.Is(err, ErrCLIExecutionInvalid) {
t.Fatalf("relative working directory error = %v", err)
}
invalid = request
invalid.WorkingDirectory += "\x00suffix"
if _, err := host.StartCLI(context.Background(), invalid); !errors.Is(err, ErrCLIExecutionInvalid) {
t.Fatalf("NUL working directory error = %v", err)
}
transport.mu.Lock()
defer transport.mu.Unlock()
if len(transport.specs) != 0 {
t.Fatalf("unexpected process starts = %d", len(transport.specs))
}
}
func TestStartCLIEnforcesManifestTimeout(t *testing.T) {
host, route, transport, request := newCLIExecutionTestHost(t)
route.cliLaunch.timeout = 20 * time.Millisecond
connection, err := host.StartCLI(context.Background(), request)
if err != nil {
t.Fatal(err)
}
transport.connection.mu.Lock()
transport.connection.blockRecv = true
transport.connection.mu.Unlock()
contextual, ok := connection.(agentruntime.ContextProcessConnection)
if !ok {
t.Fatal("timed CLI connection is not contextual")
}
if _, err := contextual.RecvContext(context.Background()); !errors.Is(err, context.DeadlineExceeded) {
t.Fatalf("timed CLI receive error = %v, want deadline exceeded", err)
}
if err := connection.Close(); err != nil {
t.Fatal(err)
}
}
func TestStartCLIUsesWorkingDirectoryForRelativePathResolution(t *testing.T) {
workspace := t.TempDir()
workingDirectory := filepath.Join(workspace, "project", "reports")
if err := os.MkdirAll(workingDirectory, 0o700); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(workingDirectory, "report.pdf"), []byte("workspace-relative-content"), 0o600); err != nil {
t.Fatal(err)
}
executable, identity := copyCLIExecutorTestExecutable(t)
processes, err := agentruntime.NewConnectorProcessTransport()
if err != nil {
t.Fatal(err)
}
generation := market.HostGeneration{BootEpoch: "boot-fixture", Generation: 1}
route := &connectorRoute{
id: connectorRouteKey("cwd-fixture", "fixture-cli"), connectionID: "cwd-fixture", connectorKey: "fixture-cli",
connectorVersion: "1.0.0", releaseDigest: cliExecutorTestDigest, generation: generation,
processes: connectorruntime.NewProcessGroup(), userHome: filepath.Join(workspace, "connector-home"),
cliContractHash: cliExecutorTestContractHash,
cliLaunch: &managedCLILaunch{
executable: connectorruntime.ConnectorExecutable{Path: executable, SHA256: identity.SHA256, SizeBytes: identity.SizeBytes},
cwd: filepath.Join(workspace, "connector-installation"), language: "native", stateDir: filepath.Join(workspace, "state"),
},
}
table := connectorruntime.NewRouteTable()
if err := table.Commit(route); err != nil {
t.Fatal(err)
}
host := &Host{routes: table, processes: processes}
connection, err := host.StartCLI(context.Background(), CLIExecutionRequest{
ConnectionID: route.connectionID, ConnectorKey: route.connectorKey, ConnectorVersion: route.connectorVersion,
ReleaseDigest: route.releaseDigest, Generation: route.generation, CLIContractHash: route.cliContractHash,
WorkingDirectory: workingDirectory, Arguments: []string{"-test.run=^TestCLIExecutorWorkingDirectoryFixture$"},
})
if err != nil {
t.Fatal(err)
}
defer connection.Close()
var stdout strings.Builder
for {
frame, err := connection.Recv()
if err != nil {
t.Fatal(err)
}
stdout.Write(frame.Stdout)
if frame.ExitCode == nil {
continue
}
if *frame.ExitCode != 0 {
t.Fatalf("fixture exit code = %d, stderr = %q", *frame.ExitCode, frame.Stderr)
}
break
}
if !strings.Contains(stdout.String(), "workspace-relative-content") {
t.Fatalf("fixture stdout = %q", stdout.String())
}
}
func TestCLIExecutorWorkingDirectoryFixture(t *testing.T) {
if os.Getenv("TUTTI_CONNECTOR_CONNECTION_ID") != "cwd-fixture" {
return
}
contents, err := os.ReadFile("report.pdf")
if err != nil {
t.Fatal(err)
}
fmt.Print(string(contents))
}
func copyCLIExecutorTestExecutable(t *testing.T) (string, *agentruntime.ExecutableIdentity) {
t.Helper()
source, err := os.Executable()
if err != nil {
t.Fatal(err)
}
contents, err := os.ReadFile(source)
if err != nil {
t.Fatal(err)
}
target := filepath.Join(t.TempDir(), "connector-cli-fixture"+filepath.Ext(source))
if err := os.WriteFile(target, contents, 0o755); err != nil {
t.Fatal(err)
}
digest := sha256.Sum256(contents)
return target, &agentruntime.ExecutableIdentity{SHA256: hex.EncodeToString(digest[:]), SizeBytes: int64(len(contents))}
}
func newCLIExecutionTestHost(t *testing.T) (*Host, *connectorRoute, *cliExecutionTransportStub, CLIExecutionRequest) {
t.Helper()
root := t.TempDir()
snapshot := filepath.Join(root, "snapshot")
generation := market.HostGeneration{BootEpoch: "boot-1", Generation: 7}
transport := &cliExecutionTransportStub{}
table := connectorruntime.NewRouteTable()
route := &connectorRoute{
id: connectorRouteKey("connection-1", "lark-cli"), connectionID: "connection-1", connectorKey: "lark-cli",
connectorVersion: "1.2.3", releaseDigest: cliExecutorTestDigest, generation: generation,
processes: connectorruntime.NewProcessGroup(), userHome: filepath.Join(root, "home", "owner"), cliContractHash: cliExecutorTestContractHash,
cliLaunch: &managedCLILaunch{
arguments: []string{filepath.Join(snapshot, "lark-cli.mjs"), "--json"}, cwd: snapshot, language: "node",
stateDir: filepath.Join(root, "state"),
executable: connectorruntime.ConnectorExecutable{Path: filepath.Join(root, "managed", "node"), SHA256: "node-digest", SizeBytes: 42},
},
}
if err := table.Commit(route); err != nil {
t.Fatal(err)
}
host := &Host{routes: table, processes: transport}
request := CLIExecutionRequest{
ConnectionID: "connection-1", ConnectorKey: "lark-cli", ConnectorVersion: "1.2.3", ReleaseDigest: cliExecutorTestDigest,
Generation: generation, CLIContractHash: cliExecutorTestContractHash,
WorkingDirectory: filepath.Join(root, "workspace", "project", "reports"),
Arguments: []string{"message", "send", "--text", "hello"},
}
return host, route, transport, request
}
@@ -0,0 +1,81 @@
package implementationhost
import (
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"testing"
market "github.com/tutti-os/tutti/packages/connector/daemon/core"
connectorruntime "github.com/tutti-os/tutti/packages/connector/runtime"
)
func TestConnectorCLIShimExecutesVerifiedEntrypointThroughNormalShellPath(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("Unix shim execution test")
}
root := t.TempDir()
working := filepath.Join(root, "working directory")
binDir := filepath.Join(root, "bin")
if err := os.MkdirAll(working, 0o700); err != nil {
t.Fatal(err)
}
entrypoint := filepath.Join(root, "connector executable")
entrypointContent := "#!/bin/sh\nprintf '%s|%s|%s|%s' \"$TUTTI_CONNECTOR_KEY\" \"$PWD\" \"$1\" \"$2\"\n"
if err := os.WriteFile(entrypoint, []byte(entrypointContent), 0o700); err != nil {
t.Fatal(err)
}
route := &connectorRoute{
connectionID: "default", connectorKey: "github", userHome: root,
generation: market.HostGeneration{BootEpoch: "boot", Generation: 1},
cliLaunch: &managedCLILaunch{executable: connectorruntime.ConnectorExecutable{Path: entrypoint},
arguments: []string{"fixed argument"}, cwd: working, stateDir: filepath.Join(root, "state"), language: "node"},
}
if err := route.prepareCLIShim(binDir); err != nil {
t.Fatal(err)
}
if err := route.activateCLIShim(); err != nil {
t.Fatal(err)
}
output, err := exec.Command(route.cliShimPath, "user argument").CombinedOutput()
if err != nil {
t.Fatalf("execute CLI shim: %v: %s", err, output)
}
want := strings.Join([]string{"github", working, "fixed argument", "user argument"}, "|")
if string(output) != want {
t.Fatalf("CLI output = %q, want %q", output, want)
}
route.removeCLIShimIfCurrent()
if _, err := os.Stat(route.cliShimPath); !os.IsNotExist(err) {
t.Fatalf("CLI shim remained after route removal: %v", err)
}
}
func TestAttachCLIUsesArtifactNativeExecutableWithoutManagedRuntimeArgument(t *testing.T) {
root := t.TempDir()
entrypointRelative := "runtime/windows-amd64/gh.exe"
entrypoint := filepath.Join(root, filepath.FromSlash(entrypointRelative))
if err := os.MkdirAll(filepath.Dir(entrypoint), 0o700); err != nil {
t.Fatal(err)
}
content := []byte("verified gh fixture")
if err := os.WriteFile(entrypoint, content, 0o600); err != nil {
t.Fatal(err)
}
managed := &market.ManagedStdioImplementation{Runtime: market.RuntimeRequirement{Language: "node"},
CLI: &market.ManagedCLIInterface{Entrypoint: entrypointRelative, Command: "gh", TimeoutMS: 30_000,
Launch: &market.CLIArtifactLaunch{Kind: market.CLIArtifactLaunchKindNative,
SHA256: strings.Repeat("a", 64), SizeBytes: int64(len(content))}}}
route := &connectorRoute{}
if err := (&Host{}).attachCLI(route, managed, market.PreparedArtifactReceipt{PreparedPath: root}, nil,
connectorruntime.ConnectorExecutable{Path: filepath.Join(root, "node")}, filepath.Join(root, "state"), nil); err != nil {
t.Fatal(err)
}
if route.cliLaunch.executable.Path != entrypoint || route.cliLaunch.executable.SHA256 != strings.Repeat("a", 64) ||
route.cliLaunch.executable.SizeBytes != int64(len(content)) || len(route.cliLaunch.arguments) != 0 ||
route.cliInvocationCommand != "gh" {
t.Fatalf("artifact-native CLI launch = %#v", route.cliLaunch)
}
}
@@ -0,0 +1,855 @@
package implementationhost
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/url"
"strings"
"sync"
"time"
"unicode/utf8"
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"
)
const credentialBrokerInitialEventTimeout = 30 * time.Second
const credentialBrokerContinuationWait = time.Second
type managedCLILaunch struct {
arguments []string
artifactTrees []agentruntime.ArtifactTreeIdentity
cwd string
executable connectorruntime.ConnectorExecutable
language string
stateDir string
timeout time.Duration
}
type credentialBrokerCLILaunch struct {
Executable string `json:"executable"`
Arguments []string `json:"arguments"`
CWD string `json:"cwd"`
}
type managedCredentialBrokerLaunch struct {
entrypoint string
timeout time.Duration
allowedHosts map[string]struct{}
cliLaunch credentialBrokerCLILaunch
executable connectorruntime.ConnectorExecutable
language string
cwd string
artifactTrees []agentruntime.ArtifactTreeIdentity
stateDir string
}
type managedCredentialAuthorizationHost interface {
authorizationRoute(context.Context, market.OperationScope, market.Connector) (*connectorRoute, error)
startCredentialBroker(context.Context, *connectorRoute, credentialBrokerRequest) (agentruntime.ProcessConnection, uint64, error)
observeAuthorization(*connectorRoute, market.AuthorizationState)
releaseAuthorizationRoute(*connectorRoute)
}
// managedCredentialAuthorizationProvider owns the host-side lifecycle of a
// connector-provided credential broker. Provider-specific commands, response
// parsing, and secret handoffs stay inside the verified connector adapter.
type managedCredentialAuthorizationProvider struct {
host managedCredentialAuthorizationHost
mu sync.Mutex
sessions map[string]*credentialBrokerSession
activeByRoute map[string]string
}
type credentialBrokerRequest struct {
Protocol string `json:"protocol"`
Operation string `json:"operation"`
}
type credentialBrokerEvent struct {
Type string `json:"type"`
URL string `json:"url,omitempty"`
Code string `json:"code,omitempty"`
Message string `json:"message,omitempty"`
}
type credentialBrokerSession struct {
operationID string
route *connectorRoute
connection agentruntime.ProcessConnection
cancel context.CancelFunc
done chan struct{}
mu sync.Mutex
state market.AuthorizationState
url string
userCode string
err error
version uint64
hasEvent bool
changed chan struct{}
}
func newManagedCredentialAuthorizationProvider(host managedCredentialAuthorizationHost) *managedCredentialAuthorizationProvider {
return &managedCredentialAuthorizationProvider{
host: host, sessions: make(map[string]*credentialBrokerSession), activeByRoute: make(map[string]string),
}
}
func (host *Host) BeginAuthorization(ctx context.Context, request market.AuthorizationStartRequest) (market.AuthorizationSession, error) {
if host == nil || host.authorizationProvider == nil {
return market.AuthorizationSession{}, errors.New("connector authorization provider is unavailable")
}
return host.authorizationProvider.Begin(ctx, request)
}
func (host *Host) DisconnectAuthorization(ctx context.Context, request market.AuthorizationDisconnectRequest) error {
if host == nil || host.authorizationProvider == nil {
return errors.New("connector authorization provider is unavailable")
}
return host.authorizationProvider.Disconnect(ctx, request)
}
func (host *Host) CancelAuthorization(ctx context.Context, request market.AuthorizationCancelRequest) error {
if host == nil || host.authorizationProvider == nil {
return errors.New("connector authorization provider is unavailable")
}
return host.authorizationProvider.Cancel(ctx, request)
}
func (host *Host) InspectAuthorization(ctx context.Context, request market.AuthorizationInspectRequest) (market.AuthorizationObservation, error) {
if host == nil || host.authorizationProvider == nil {
return market.AuthorizationObservation{}, errors.New("connector authorization inspector is unavailable")
}
return host.authorizationProvider.Inspect(ctx, request)
}
func (provider *managedCredentialAuthorizationProvider) Begin(
ctx context.Context,
request market.AuthorizationStartRequest,
) (market.AuthorizationSession, error) {
connector := request.Connector
connector.Release = request.Release
route, err := provider.host.authorizationRoute(ctx, request.Scope, connector)
if err != nil {
return market.AuthorizationSession{}, err
}
session, err := provider.authorizationSessionOrStart(route, request.OperationID, request.StepRevisionBase)
if err != nil {
return market.AuthorizationSession{}, err
}
if err := session.awaitEventAfter(ctx, request.AfterStepRevision); err != nil {
cleanupContext, cancelCleanup := context.WithTimeout(context.Background(), 5*time.Second)
cleanupErr := provider.cancelAuthorizationSession(cleanupContext, session)
cancelCleanup()
return market.AuthorizationSession{}, errors.Join(err, cleanupErr)
}
state, authorizationURL, userCode, stepRevision, sessionErr := session.snapshot()
if sessionErr != nil {
provider.clearAuthorizationSession(request.OperationID, session)
provider.host.releaseAuthorizationRoute(route)
return market.AuthorizationSession{}, sessionErr
}
result := market.AuthorizationSession{
OperationID: request.OperationID,
ConnectorKey: request.Connector.Key,
ConnectionID: route.connectionID,
SessionID: request.OperationID + "/credential-broker",
AuthorizationURL: authorizationURL,
UserCode: userCode,
StepRevision: stepRevision,
State: state,
}
if state == market.AuthorizationStateConnected {
provider.clearAuthorizationSession(request.OperationID, session)
provider.host.releaseAuthorizationRoute(route)
}
return result, nil
}
func (provider *managedCredentialAuthorizationProvider) Cancel(
ctx context.Context,
request market.AuthorizationCancelRequest,
) error {
provider.mu.Lock()
session := provider.sessions[strings.TrimSpace(request.OperationID)]
provider.mu.Unlock()
if session == nil {
return nil
}
return provider.cancelAuthorizationSession(ctx, session)
}
func (provider *managedCredentialAuthorizationProvider) Disconnect(
ctx context.Context,
request market.AuthorizationDisconnectRequest,
) error {
route, err := provider.host.authorizationRoute(ctx, request.Scope, request.Connector)
if err != nil {
return err
}
if session := provider.takeAuthorizationSessionByRoute(route.id); session != nil {
session.cancel()
select {
case <-session.done:
case <-ctx.Done():
return ctx.Err()
}
}
operationContext, cancel := context.WithTimeout(ctx, route.credentialBrokerLaunch.timeout)
defer cancel()
connection, processID, err := provider.host.startCredentialBroker(operationContext, route, credentialBrokerRequest{
Protocol: market.CredentialBrokerProtocolV1, Operation: "disconnect",
})
if err != nil {
return fmt.Errorf("start connector credential broker disconnect: %w", err)
}
defer func() { _ = route.releaseProcess(processID, connection) }()
event, err := readCredentialBrokerTerminalEvent(operationContext, connection)
if err != nil {
return fmt.Errorf("disconnect connector authorization: %w", err)
}
if event.Type != "disconnected" {
return credentialBrokerEventError(event, "disconnect")
}
provider.host.observeAuthorization(route, market.AuthorizationStateDisconnected)
provider.host.releaseAuthorizationRoute(route)
return nil
}
func (provider *managedCredentialAuthorizationProvider) Inspect(
ctx context.Context,
request market.AuthorizationInspectRequest,
) (market.AuthorizationObservation, error) {
route, err := provider.host.authorizationRoute(ctx, request.Scope, request.Connector)
if err != nil {
return market.AuthorizationObservation{}, err
}
operationContext, cancel := context.WithTimeout(ctx, route.credentialBrokerLaunch.timeout)
defer cancel()
connection, processID, err := provider.host.startCredentialBroker(operationContext, route, credentialBrokerRequest{
Protocol: market.CredentialBrokerProtocolV1, Operation: "inspect",
})
if err != nil {
return market.AuthorizationObservation{}, fmt.Errorf("start connector credential broker inspect: %w", err)
}
defer func() { _ = route.releaseProcess(processID, connection) }()
event, err := readCredentialBrokerInspectionEvent(operationContext, connection)
if err != nil {
return market.AuthorizationObservation{}, fmt.Errorf("inspect connector authorization: %w", err)
}
var state market.AuthorizationObservationState
switch event.Type {
case "connected":
state = market.AuthorizationObservationConnected
case "disconnected":
state = market.AuthorizationObservationDisconnected
case "expired":
state = market.AuthorizationObservationExpired
case "error":
state = market.AuthorizationObservationFailed
default:
return market.AuthorizationObservation{}, fmt.Errorf("connector credential broker returned unsupported inspect event %q", event.Type)
}
connector := request.Connector
return market.AuthorizationObservation{
AccountID: request.Scope.AccountID, AccountGeneration: request.AccountGeneration,
VMAssignmentID: request.VMAssignmentID, ConnectorKey: connector.Key, ConnectionID: route.connectionID,
ReleaseDigest: connector.Release.ReleaseDigest, AuthorizationSessionID: request.AuthorizationSessionID,
AuthorizationGeneration: request.AuthorizationGeneration, DesktopBootEpoch: request.DesktopBootEpoch,
GuestBootID: request.GuestBootID, RuntimeEpoch: request.RuntimeEpoch, StateRevision: request.StateRevision,
State: state, Reason: strings.TrimSpace(event.Message), FailureCode: strings.TrimSpace(event.Code), ObservedAt: time.Now().UTC(),
}, nil
}
func (provider *managedCredentialAuthorizationProvider) authorizationSessionOrStart(
route *connectorRoute,
operationID string,
stepRevisionBase uint64,
) (*credentialBrokerSession, error) {
operationID = strings.TrimSpace(operationID)
provider.mu.Lock()
if session := provider.sessions[operationID]; session != nil {
_, _, _, _, sessionErr := session.snapshot()
if sessionErr == nil {
provider.mu.Unlock()
return session, nil
}
// A broker can fail after emitting its first authorization URL. Do not
// make the next user action consume that terminal error merely to clear
// the cache; replace it with a fresh broker in this same request.
delete(provider.sessions, operationID)
if provider.activeByRoute[route.id] == operationID {
delete(provider.activeByRoute, route.id)
}
}
if activeOperationID := provider.activeByRoute[route.id]; activeOperationID != "" {
active := provider.sessions[activeOperationID]
if active != nil {
_, _, _, _, activeErr := active.snapshot()
if activeErr != nil {
select {
case <-active.done:
delete(provider.sessions, activeOperationID)
delete(provider.activeByRoute, route.id)
default:
provider.mu.Unlock()
return nil, errors.New("connector credential broker termination is still pending")
}
} else {
provider.mu.Unlock()
return nil, errors.New("connector credential broker has an active authorization attempt")
}
} else {
delete(provider.activeByRoute, route.id)
}
}
processContext, cancel := context.WithTimeout(context.Background(), route.credentialBrokerLaunch.timeout)
connection, processID, err := provider.host.startCredentialBroker(processContext, route, credentialBrokerRequest{
Protocol: market.CredentialBrokerProtocolV1, Operation: "begin",
})
if err != nil {
cancel()
provider.mu.Unlock()
return nil, fmt.Errorf("start connector credential broker: %w", err)
}
session := &credentialBrokerSession{
operationID: operationID, route: route, connection: connection, cancel: cancel,
done: make(chan struct{}), version: stepRevisionBase, changed: make(chan struct{}),
}
provider.sessions[operationID] = session
provider.activeByRoute[route.id] = operationID
provider.mu.Unlock()
go consumeAuthorizationEvents(processContext, provider.host, route, connection, processID, session)
return session, nil
}
func consumeAuthorizationEvents(
ctx context.Context,
host managedCredentialAuthorizationHost,
route *connectorRoute,
connection agentruntime.ProcessConnection,
processID uint64,
session *credentialBrokerSession,
) {
defer session.cancel()
defer close(session.done)
defer func() { _ = route.releaseProcess(processID, connection) }()
var stdout, stderr strings.Builder
for {
frame, err := receiveCredentialBrokerFrameContext(ctx, connection)
if err != nil {
// Explicit cancellation owns the terminal state transition. Do not race
// the caller by projecting a transient broker failure while shutdown is
// already in progress.
if errors.Is(ctx.Err(), context.Canceled) {
return
}
if !errors.Is(err, io.EOF) {
failAuthorizationSession(host, route, session, fmt.Errorf("receive connector credential broker event: %w", err))
} else if !session.terminal() {
failAuthorizationSession(host, route, session, errors.New("connector credential broker exited before a terminal event"))
}
return
}
stdout.Write(frame.Stdout)
stderr.Write(frame.Stderr)
if stdout.Len()+stderr.Len() > 1<<20 {
failAuthorizationSession(host, route, session, errors.New("connector credential broker output exceeded its limit"))
return
}
for {
line, remaining, ok := strings.Cut(stdout.String(), "\n")
if !ok {
break
}
stdout.Reset()
stdout.WriteString(remaining)
if err := applyCredentialBrokerEvent(host, route, session, line); err != nil {
failAuthorizationSession(host, route, session, err)
return
}
}
if frame.ExitCode != nil {
if trailing := strings.TrimSpace(stdout.String()); trailing != "" {
if err := applyCredentialBrokerEvent(host, route, session, trailing); err != nil {
failAuthorizationSession(host, route, session, err)
return
}
}
if *frame.ExitCode != 0 && !session.terminal() {
failAuthorizationSession(host, route, session, fmt.Errorf("connector credential broker exited with code %d: %s", *frame.ExitCode, boundedBrokerMessage(stderr.String())))
} else if !session.terminal() {
failAuthorizationSession(host, route, session, errors.New("connector credential broker exited before a terminal event"))
}
return
}
}
}
func applyCredentialBrokerEvent(host managedCredentialAuthorizationHost, route *connectorRoute, session *credentialBrokerSession, payload string) error {
if strings.TrimSpace(payload) == "" {
return nil
}
var event credentialBrokerEvent
if err := decodeCredentialBrokerEvent(payload, &event); err != nil {
return fmt.Errorf("decode connector credential broker event: %w", err)
}
switch event.Type {
case "authorization_url":
if !safeCredentialBrokerURL(event.URL, route.credentialBrokerLaunch.allowedHosts) {
return errors.New("connector credential broker returned an unauthorized URL")
}
userCode, err := normalizeCredentialBrokerUserCode(event.Code)
if err != nil {
return err
}
session.update(market.AuthorizationStatePending, event.URL, userCode, nil)
host.observeAuthorization(route, market.AuthorizationStatePending)
case "connected":
session.update(market.AuthorizationStateConnected, "", "", nil)
host.observeAuthorization(route, market.AuthorizationStateConnected)
case "error":
return credentialBrokerEventError(event, "authorize")
default:
return fmt.Errorf("connector credential broker returned unsupported event type %q", event.Type)
}
return nil
}
func (host *Host) authorizationRoute(ctx context.Context, scope market.OperationScope, connector market.Connector) (*connectorRoute, error) {
if host == nil {
return nil, errors.New("managed connector authorization is unavailable")
}
releaseLane := host.enterConnectorLane(connector.Key)
defer releaseLane()
managed := connector.Release.Manifest.Implementation.ManagedStdio
if connector.Release.Manifest.Implementation.Kind != market.ImplementationKindManagedStdio ||
connector.Release.Manifest.AuthorizationKind == "none" || managed == nil || managed.CredentialBroker == nil ||
!installationTargetsRelease(connector.Installation, connector.Release.ReleaseDigest) {
return nil, errors.New("managed connector authorization is unavailable")
}
connectionID := "default"
if accountID := strings.TrimSpace(scope.AccountID); accountID != "" {
connectionID = market.AccountRuntimeConnectionID(accountID, connector.Key)
}
key := connectorRouteKey(connectionID, connector.Key)
if route, _ := host.routes.Route(key).(*connectorRoute); route != nil && host.routeCurrent(route) && route.credentialBrokerLaunch != nil {
return route, nil
}
host.authorizationMu.Lock()
if route := host.authorizationRoutes[key]; route != nil && route.releaseDigest == connector.Release.ReleaseDigest &&
route.credentialBrokerLaunch != nil && !route.processes.IsFenced() {
host.authorizationMu.Unlock()
return route, nil
}
host.authorizationMu.Unlock()
route, err := host.buildAuthorizationRoute(ctx, connectionID, connector)
if err != nil {
return nil, err
}
host.authorizationMu.Lock()
current := host.authorizationRoutes[key]
if current == nil || current.releaseDigest != connector.Release.ReleaseDigest || current.processes.IsFenced() {
host.authorizationRoutes[key] = route
host.authorizationMu.Unlock()
if current != nil {
current.Fence()
_ = current.Close(time.Now().Add(3 * time.Second))
}
return route, nil
}
host.authorizationMu.Unlock()
route.Fence()
_ = route.Close(time.Now().Add(3 * time.Second))
return current, nil
}
func (host *Host) buildAuthorizationRoute(ctx context.Context, connectionID string, connector market.Connector) (*connectorRoute, error) {
if err := market.ValidateRuntimeReleaseShape(connector.Release); err != nil {
return nil, err
}
if !installationTargetsRelease(connector.Installation, connector.Release.ReleaseDigest) {
return nil, errors.New("managed connector authorization release is not installed")
}
prepared, err := host.artifacts.ResolvePrepared(ctx, connector.Release)
if err != nil {
return nil, fmt.Errorf("resolve prepared connector artifact for authorization: %w", err)
}
installedRoot := prepared.PreparedPath
executionRoot, err := host.snapshots.Create(prepared, artifactNativeEntrypoints(connector.Release)...)
if err != nil {
return nil, fmt.Errorf("create connector authorization snapshot: %w", err)
}
prepared.PreparedPath = executionRoot
generation := connector.Revision
if generation == 0 {
generation = 1
}
runtimeRequest := market.RuntimeReconcileRequest{
ConnectionID: connectionID,
Connector: connector,
Generation: market.HostGeneration{BootEpoch: "authorization", Generation: generation},
}
plan, err := host.planner.Build(ctx, runtimeRequest, prepared)
if err != nil {
_ = host.snapshots.Remove(executionRoot)
return nil, err
}
route := newConnectorRoute(runtimeRequest)
route.userHome = plan.UserHome
route.executionRoot = executionRoot
route.installedRoot = installedRoot
route.snapshots = host.snapshots
if plan.Managed.CLI == nil || plan.Managed.CredentialBroker == nil {
_ = route.Close(time.Now().Add(3 * time.Second))
return nil, errors.New("managed connector authorization requires a CLI credential broker")
}
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 := 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
}
return route, nil
}
func (host *Host) observeAuthorization(route *connectorRoute, state market.AuthorizationState) {
if host == nil || host.authorizationObserver == nil || route == nil {
return
}
host.authorizationObserver.ObserveAuthorization(context.Background(), AuthorizationObservation{
ConnectorKey: route.connectorKey, ConnectionID: route.connectionID, State: state, ObservedAt: time.Now().UTC(),
})
}
func (host *Host) releaseAuthorizationRoute(route *connectorRoute) {
if host == nil || route == nil {
return
}
host.authorizationMu.Lock()
if host.authorizationRoutes[route.id] == route {
delete(host.authorizationRoutes, route.id)
} else {
host.authorizationMu.Unlock()
return
}
host.authorizationMu.Unlock()
route.Fence()
_ = route.Close(time.Now().Add(3 * time.Second))
}
func (host *Host) releaseAuthorizationRouteByKey(key string) {
if host == nil {
return
}
host.authorizationMu.Lock()
route := host.authorizationRoutes[key]
delete(host.authorizationRoutes, key)
host.authorizationMu.Unlock()
if route != nil {
route.Fence()
_ = route.Close(time.Now().Add(3 * time.Second))
}
}
func failAuthorizationSession(host managedCredentialAuthorizationHost, route *connectorRoute, session *credentialBrokerSession, err error) {
session.fail(err)
host.observeAuthorization(route, market.AuthorizationStateFailed)
}
func (host *Host) startCredentialBroker(
ctx context.Context,
route *connectorRoute,
request credentialBrokerRequest,
) (agentruntime.ProcessConnection, uint64, error) {
launch := route.credentialBrokerLaunch
if launch == nil || request.Protocol != market.CredentialBrokerProtocolV1 ||
(request.Operation != "begin" && request.Operation != "inspect" && request.Operation != "disconnect") {
return nil, 0, errors.New("connector credential broker request is invalid")
}
spec := connectorruntime.ConnectorProcessSpec(route.connectionID, route.connectorKey, launch.language, launch.executable,
launch.cwd, []string{launch.entrypoint}, launch.stateDir, route.userHome, launch.artifactTrees)
cliLaunch, err := json.Marshal(launch.cliLaunch)
if err != nil {
return nil, 0, err
}
spec.Env = append(spec.Env,
"TUTTI_CONNECTOR_CREDENTIAL_BROKER_PROTOCOL="+market.CredentialBrokerProtocolV1,
"TUTTI_CONNECTOR_CLI_LAUNCH_JSON="+string(cliLaunch),
)
connection, processID, err := host.startProcess(ctx, route, spec, false)
if err != nil {
return nil, 0, err
}
payload, err := json.Marshal(request)
if err == nil {
err = connection.Send(append(payload, '\n'))
}
if err == nil {
if graceful, ok := connection.(agentruntime.GracefulProcessConnection); ok {
err = graceful.CloseInput()
}
}
if err != nil {
_ = route.releaseProcess(processID, connection)
return nil, 0, err
}
return connection, processID, nil
}
func receiveCredentialBrokerFrameContext(ctx context.Context, connection agentruntime.ProcessConnection) (agentruntime.ProcessFrame, error) {
if contextual, ok := connection.(agentruntime.ContextProcessConnection); ok {
return contextual.RecvContext(ctx)
}
return connection.Recv()
}
func readCredentialBrokerTerminalEvent(ctx context.Context, connection agentruntime.ProcessConnection) (credentialBrokerEvent, error) {
var output strings.Builder
for {
frame, err := receiveCredentialBrokerFrameContext(ctx, connection)
if err != nil {
return credentialBrokerEvent{}, err
}
output.Write(frame.Stdout)
if output.Len() > 1<<20 {
return credentialBrokerEvent{}, errors.New("connector credential broker output exceeded its limit")
}
for {
line, remaining, ok := strings.Cut(output.String(), "\n")
if !ok {
break
}
output.Reset()
output.WriteString(remaining)
if strings.TrimSpace(line) == "" {
continue
}
var event credentialBrokerEvent
if err := decodeCredentialBrokerEvent(line, &event); err != nil {
return credentialBrokerEvent{}, err
}
if event.Type == "disconnected" || event.Type == "error" {
return event, nil
}
return credentialBrokerEvent{}, fmt.Errorf("unexpected credential broker disconnect event %q", event.Type)
}
if frame.ExitCode != nil {
return credentialBrokerEvent{}, errors.New("connector credential broker exited before disconnect completed")
}
}
}
func readCredentialBrokerInspectionEvent(ctx context.Context, connection agentruntime.ProcessConnection) (credentialBrokerEvent, error) {
var output strings.Builder
for {
frame, err := receiveCredentialBrokerFrameContext(ctx, connection)
if err != nil {
return credentialBrokerEvent{}, err
}
output.Write(frame.Stdout)
if output.Len() > 1<<20 {
return credentialBrokerEvent{}, errors.New("connector credential broker output exceeded its limit")
}
for {
line, remaining, ok := strings.Cut(output.String(), "\n")
if !ok {
break
}
output.Reset()
output.WriteString(remaining)
if strings.TrimSpace(line) == "" {
continue
}
var event credentialBrokerEvent
if err := decodeCredentialBrokerEvent(line, &event); err != nil {
return credentialBrokerEvent{}, err
}
switch event.Type {
case "connected", "disconnected", "expired", "error":
return event, nil
default:
return credentialBrokerEvent{}, fmt.Errorf("unexpected credential broker inspect event %q", event.Type)
}
}
if frame.ExitCode != nil {
return credentialBrokerEvent{}, errors.New("connector credential broker exited before inspect completed")
}
}
}
func decodeCredentialBrokerEvent(payload string, event *credentialBrokerEvent) error {
decoder := json.NewDecoder(strings.NewReader(payload))
decoder.DisallowUnknownFields()
if err := decoder.Decode(event); err != nil {
return err
}
if err := decoder.Decode(&struct{}{}); !errors.Is(err, io.EOF) {
return errors.New("credential broker event contains trailing JSON")
}
return nil
}
func safeCredentialBrokerURL(value string, allowedHosts map[string]struct{}) bool {
parsed, err := url.Parse(strings.TrimSpace(value))
if err != nil || parsed.Scheme != "https" || parsed.User != nil || parsed.Hostname() == "" || parsed.Port() != "" {
return false
}
_, allowed := allowedHosts[strings.ToLower(parsed.Hostname())]
return allowed
}
func credentialBrokerEventError(event credentialBrokerEvent, operation string) error {
message := boundedBrokerMessage(event.Message)
if message == "" {
message = "connector credential broker reported an error"
}
if code := strings.TrimSpace(event.Code); code != "" {
return fmt.Errorf("%s connector authorization (%s): %s", operation, code, message)
}
return fmt.Errorf("%s connector authorization: %s", operation, message)
}
func boundedBrokerMessage(message string) string {
message = strings.TrimSpace(message)
if len(message) > 4096 {
message = message[len(message)-4096:]
}
return message
}
func normalizeCredentialBrokerUserCode(value string) (string, error) {
value = strings.TrimSpace(value)
if utf8.RuneCountInString(value) > 128 {
return "", errors.New("connector credential broker returned an oversized device code")
}
return value, nil
}
func (session *credentialBrokerSession) awaitEventAfter(ctx context.Context, afterStepRevision uint64) error {
session.mu.Lock()
hasEvent := session.hasEvent
session.mu.Unlock()
wait := credentialBrokerContinuationWait
if !hasEvent {
wait = credentialBrokerInitialEventTimeout
}
timer := time.NewTimer(wait)
defer timer.Stop()
for {
session.mu.Lock()
hasEvent = session.hasEvent
version := session.version
state := session.state
changed := session.changed
session.mu.Unlock()
if hasEvent && (version > afterStepRevision || state == market.AuthorizationStateConnected || state == market.AuthorizationStateFailed) {
return nil
}
select {
case <-changed:
continue
case <-ctx.Done():
return ctx.Err()
case <-timer.C:
session.mu.Lock()
hasEvent = session.hasEvent
session.mu.Unlock()
if !hasEvent {
return errors.New("connector credential broker did not return an initial event")
}
return nil
}
}
}
func (session *credentialBrokerSession) update(state market.AuthorizationState, authorizationURL, userCode string, err error) {
session.mu.Lock()
session.state = state
session.url = authorizationURL
session.userCode = userCode
session.err = err
session.version++
session.hasEvent = true
close(session.changed)
session.changed = make(chan struct{})
session.mu.Unlock()
}
func (session *credentialBrokerSession) fail(err error) {
session.update(market.AuthorizationStateFailed, "", "", err)
}
func (session *credentialBrokerSession) snapshot() (market.AuthorizationState, string, string, uint64, error) {
session.mu.Lock()
defer session.mu.Unlock()
return session.state, session.url, session.userCode, session.version, session.err
}
func (session *credentialBrokerSession) terminal() bool {
session.mu.Lock()
defer session.mu.Unlock()
return session.state == market.AuthorizationStateConnected || session.err != nil
}
func (provider *managedCredentialAuthorizationProvider) takeAuthorizationSessionByRoute(routeID string) *credentialBrokerSession {
provider.mu.Lock()
defer provider.mu.Unlock()
operationID := provider.activeByRoute[routeID]
session := provider.sessions[operationID]
delete(provider.activeByRoute, routeID)
delete(provider.sessions, operationID)
return session
}
func (provider *managedCredentialAuthorizationProvider) cancelAuthorizationSession(
ctx context.Context,
session *credentialBrokerSession,
) error {
if provider == nil || session == nil {
return nil
}
session.cancel()
var closeErr error
if session.connection != nil {
closeErr = session.connection.Close()
}
select {
case <-session.done:
provider.clearAuthorizationSession(session.operationID, session)
provider.host.releaseAuthorizationRoute(session.route)
return closeErr
case <-ctx.Done():
return errors.Join(closeErr, fmt.Errorf("wait for connector credential broker termination: %w", ctx.Err()))
}
}
func (provider *managedCredentialAuthorizationProvider) cancelAuthorizationSessionByRoute(routeID string) {
if provider == nil {
return
}
if session := provider.takeAuthorizationSessionByRoute(routeID); session != nil {
session.cancel()
if session.connection != nil {
_ = session.connection.Close()
}
}
}
func (provider *managedCredentialAuthorizationProvider) clearAuthorizationSession(operationID string, session *credentialBrokerSession) {
provider.mu.Lock()
if provider.sessions[operationID] == session {
delete(provider.sessions, operationID)
}
if session != nil && provider.activeByRoute[session.route.id] == operationID {
delete(provider.activeByRoute, session.route.id)
}
provider.mu.Unlock()
}
@@ -0,0 +1,86 @@
package implementationhost
import (
"context"
"errors"
"strings"
"time"
market "github.com/tutti-os/tutti/packages/connector/daemon/core"
)
func (host *Host) ObserveAuthorization(ctx context.Context, request market.AuthorizationObserveRequest) (market.AuthorizationObservation, error) {
if host == nil || host.authorizationProvider == nil {
return market.AuthorizationObservation{}, errors.New("connector authorization observer is unavailable")
}
return host.authorizationProvider.Observe(ctx, request)
}
func (provider *managedCredentialAuthorizationProvider) Observe(
ctx context.Context,
request market.AuthorizationObserveRequest,
) (market.AuthorizationObservation, error) {
connector := request.Connector
connector.Release = request.Release
route, err := provider.host.authorizationRoute(ctx, request.Scope, connector)
if err != nil {
return market.AuthorizationObservation{}, err
}
if session := provider.activeAuthorizationSession(route, request.Session); session != nil {
state, _, _, _, sessionErr := session.snapshot()
observationState := market.AuthorizationObservationPending
failureCode := ""
reason := ""
switch state {
case market.AuthorizationStateConnected:
observationState = market.AuthorizationObservationConnected
case market.AuthorizationStateFailed:
observationState = market.AuthorizationObservationFailed
failureCode = "credential_broker_failed"
}
if sessionErr != nil {
observationState = market.AuthorizationObservationFailed
failureCode = "credential_broker_failed"
reason = boundedBrokerMessage(sessionErr.Error())
}
return market.AuthorizationObservation{
AccountID: request.Scope.AccountID, ConnectorKey: connector.Key, ConnectionID: route.connectionID,
ReleaseDigest: request.Release.ReleaseDigest, AuthorizationSessionID: request.Session.SessionID,
State: observationState, Reason: reason, FailureCode: failureCode, ObservedAt: time.Now().UTC(),
}, nil
}
observation, err := provider.Inspect(ctx, market.AuthorizationInspectRequest{
Scope: request.Scope, Connector: connector, AuthorizationSessionID: request.Session.SessionID,
})
if err != nil {
return market.AuthorizationObservation{}, err
}
switch observation.State {
case market.AuthorizationObservationDisconnected:
observation.State = market.AuthorizationObservationPending
case market.AuthorizationObservationExpired:
observation.State = market.AuthorizationObservationFailed
if observation.FailureCode == "" {
observation.FailureCode = "connector_authorization_expired"
}
}
return observation, nil
}
func (provider *managedCredentialAuthorizationProvider) activeAuthorizationSession(
route *connectorRoute,
wanted market.AuthorizationSession,
) *credentialBrokerSession {
operationID := strings.TrimSpace(wanted.OperationID)
if route == nil || operationID == "" || strings.TrimSpace(wanted.SessionID) != operationID+"/credential-broker" {
return nil
}
provider.mu.Lock()
defer provider.mu.Unlock()
session := provider.sessions[operationID]
if session == nil || session.route == nil || session.route.id != route.id {
return nil
}
return session
}
@@ -0,0 +1,645 @@
package implementationhost
import (
"context"
"encoding/json"
"errors"
"io"
"os"
"path/filepath"
"reflect"
"strings"
"sync"
"testing"
"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"
)
func TestAttachCredentialBrokerPreservesEmptyNativeCLIArguments(t *testing.T) {
preparedPath := t.TempDir()
if err := os.WriteFile(filepath.Join(preparedPath, "credential-broker.mjs"), []byte("export {};\n"), 0o600); err != nil {
t.Fatal(err)
}
route := &connectorRoute{cliLaunch: &managedCLILaunch{
arguments: []string{}, cwd: preparedPath,
executable: connectorruntime.ConnectorExecutable{Path: "/managed/lark-cli"},
}}
err := (&Host{}).attachCredentialBroker(route, &market.ManagedCredentialBroker{
Entrypoint: "credential-broker.mjs", TimeoutMS: 300_000,
}, market.PreparedArtifactReceipt{PreparedPath: preparedPath}, connectorruntime.ConnectorExecutable{}, "", nil)
if err != nil {
t.Fatal(err)
}
payload, err := json.Marshal(route.credentialBrokerLaunch.cliLaunch)
if err != nil {
t.Fatal(err)
}
if !strings.Contains(string(payload), `"arguments":[]`) {
t.Fatalf("credential broker CLI launch = %s, want empty JSON array", payload)
}
}
type credentialAuthorizationHostStub struct {
mu sync.Mutex
route *connectorRoute
connections []agentruntime.ProcessConnection
requests []credentialBrokerRequest
observed []market.AuthorizationState
}
func (stub *credentialAuthorizationHostStub) authorizationRoute(context.Context, market.OperationScope, market.Connector) (*connectorRoute, error) {
if stub.route == nil {
return nil, errors.New("route unavailable")
}
return stub.route, nil
}
func (stub *credentialAuthorizationHostStub) observeAuthorization(_ *connectorRoute, state market.AuthorizationState) {
stub.mu.Lock()
defer stub.mu.Unlock()
stub.observed = append(stub.observed, state)
}
func (stub *credentialAuthorizationHostStub) authorizationObservations() []market.AuthorizationState {
stub.mu.Lock()
defer stub.mu.Unlock()
return append([]market.AuthorizationState(nil), stub.observed...)
}
func (*credentialAuthorizationHostStub) releaseAuthorizationRoute(*connectorRoute) {}
func (stub *credentialAuthorizationHostStub) startCredentialBroker(
_ context.Context,
_ *connectorRoute,
request credentialBrokerRequest,
) (agentruntime.ProcessConnection, uint64, error) {
stub.requests = append(stub.requests, request)
if len(stub.connections) == 0 {
return nil, 0, errors.New("unexpected credential broker start")
}
connection := stub.connections[0]
stub.connections = stub.connections[1:]
return connection, uint64(len(stub.requests)), nil
}
type credentialBrokerConnection struct {
frames chan agentruntime.ProcessFrame
closed chan struct{}
closeOnce sync.Once
}
func newCredentialBrokerConnection() *credentialBrokerConnection {
return &credentialBrokerConnection{frames: make(chan agentruntime.ProcessFrame, 8), closed: make(chan struct{})}
}
func (*credentialBrokerConnection) Send([]byte) error { return nil }
func (*credentialBrokerConnection) CloseInput() error { return nil }
func (*credentialBrokerConnection) Terminate() error { return nil }
func (*credentialBrokerConnection) Kill() error { return nil }
func (connection *credentialBrokerConnection) Close() error {
connection.closeOnce.Do(func() {
close(connection.closed)
close(connection.frames)
})
return nil
}
func (connection *credentialBrokerConnection) Recv() (agentruntime.ProcessFrame, error) {
frame, ok := <-connection.frames
if !ok {
return agentruntime.ProcessFrame{}, io.EOF
}
return frame, nil
}
func (connection *credentialBrokerConnection) RecvContext(ctx context.Context) (agentruntime.ProcessFrame, error) {
select {
case <-ctx.Done():
return agentruntime.ProcessFrame{}, ctx.Err()
case frame, ok := <-connection.frames:
if !ok {
return agentruntime.ProcessFrame{}, io.EOF
}
return frame, nil
}
}
func TestManagedCredentialAuthorizationContinuesConnectorOwnedBroker(t *testing.T) {
connection := newCredentialBrokerConnection()
host := &credentialAuthorizationHostStub{
route: &connectorRoute{id: "default\x00lark-cli", credentialBrokerLaunch: &managedCredentialBrokerLaunch{
timeout: 5 * time.Minute,
allowedHosts: map[string]struct{}{
"open.feishu.cn": {},
"accounts.feishu.cn": {},
},
}},
connections: []agentruntime.ProcessConnection{connection},
}
provider := newManagedCredentialAuthorizationProvider(host)
request := market.AuthorizationStartRequest{OperationID: "authorize-1", Connector: market.Connector{Key: "lark-cli"}}
firstResult := make(chan market.AuthorizationSession, 1)
firstError := make(chan error, 1)
go func() {
session, err := provider.Begin(context.Background(), request)
firstResult <- session
firstError <- err
}()
connection.frames <- agentruntime.ProcessFrame{Stdout: []byte(`{"type":"authorization_url","url":"https://open.feishu.cn/page/cli?user_code=opaque"}` + "\n")}
if err := <-firstError; err != nil {
t.Fatal(err)
}
first := <-firstResult
if first.State != market.AuthorizationStatePending || first.AuthorizationURL != "https://open.feishu.cn/page/cli?user_code=opaque" {
t.Fatalf("first session = %#v", first)
}
if first.StepRevision != 1 {
t.Fatalf("first step revision = %d, want 1", first.StepRevision)
}
secondResult := make(chan market.AuthorizationSession, 1)
secondError := make(chan error, 1)
go func() {
continuedRequest := request
continuedRequest.AfterStepRevision = first.StepRevision
session, err := provider.Begin(context.Background(), continuedRequest)
secondResult <- session
secondError <- err
}()
select {
case result := <-secondResult:
t.Fatalf("continuation returned the current step before a later event: %#v", result)
case <-time.After(20 * time.Millisecond):
}
connection.frames <- agentruntime.ProcessFrame{Stdout: []byte(`{"type":"authorization_url","url":"https://accounts.feishu.cn/device?user_code=user"}` + "\n")}
if err := <-secondError; err != nil {
t.Fatal(err)
}
second := <-secondResult
if second.State != market.AuthorizationStatePending {
t.Fatalf("second session = %#v", second)
}
if second.AuthorizationURL != "https://accounts.feishu.cn/device?user_code=user" || second.StepRevision != 2 {
t.Fatalf("second step = %#v", second)
}
exitCode := 0
connection.frames <- agentruntime.ProcessFrame{Stdout: []byte(`{"type":"connected"}` + "\n"), ExitCode: &exitCode}
terminalRequest := request
terminalRequest.AfterStepRevision = second.StepRevision
connected := awaitAuthorizationSession(t, provider, terminalRequest, "")
if connected.State != market.AuthorizationStateConnected {
t.Fatalf("connected session = %#v", connected)
}
if connected.StepRevision != 3 {
t.Fatalf("connected step revision = %d, want 3", connected.StepRevision)
}
if !reflect.DeepEqual(host.requests, []credentialBrokerRequest{{Protocol: market.CredentialBrokerProtocolV1, Operation: "begin"}}) {
t.Fatalf("broker requests = %#v", host.requests)
}
observed := awaitAuthorizationObservations(t, host, 3)
if !reflect.DeepEqual(observed, []market.AuthorizationState{market.AuthorizationStatePending, market.AuthorizationStatePending, market.AuthorizationStateConnected}) {
t.Fatalf("authorization observations = %#v", observed)
}
}
func TestManagedCredentialAuthorizationObservesActiveBrokerWithoutConcurrentInspection(t *testing.T) {
connection := newCredentialBrokerConnection()
route := &connectorRoute{
id: "account-1\x00lark-cli", connectorKey: "lark-cli", connectionID: "account-1",
releaseDigest: strings.Repeat("a", 64), credentialBrokerLaunch: &managedCredentialBrokerLaunch{
timeout: 5 * time.Minute, allowedHosts: map[string]struct{}{"accounts.feishu.cn": {}},
},
}
host := &credentialAuthorizationHostStub{
route: route, connections: []agentruntime.ProcessConnection{connection},
}
provider := newManagedCredentialAuthorizationProvider(host)
connector := market.Connector{Key: "lark-cli", Release: market.Release{ReleaseDigest: route.releaseDigest}}
request := market.AuthorizationStartRequest{
OperationID: "authorize-lark", Scope: market.OperationScope{AccountID: "user-1"}, Connector: connector,
}
beginResult := make(chan market.AuthorizationSession, 1)
beginError := make(chan error, 1)
go func() {
session, err := provider.Begin(context.Background(), request)
beginResult <- session
beginError <- err
}()
connection.frames <- agentruntime.ProcessFrame{Stdout: []byte(`{"type":"authorization_url","url":"https://accounts.feishu.cn/device"}` + "\n")}
if err := <-beginError; err != nil {
t.Fatal(err)
}
session := <-beginResult
observation, err := provider.Observe(context.Background(), market.AuthorizationObserveRequest{
Scope: request.Scope, Connector: connector, Release: connector.Release, Session: session,
})
if err != nil {
t.Fatal(err)
}
if observation.State != market.AuthorizationObservationPending || observation.ConnectionID != route.connectionID ||
observation.AuthorizationSessionID != session.SessionID {
t.Fatalf("active authorization observation = %#v", observation)
}
if !reflect.DeepEqual(host.requests, []credentialBrokerRequest{{Protocol: market.CredentialBrokerProtocolV1, Operation: "begin"}}) {
t.Fatalf("broker requests = %#v, want no concurrent inspection", host.requests)
}
if err := provider.Cancel(context.Background(), market.AuthorizationCancelRequest{OperationID: request.OperationID}); err != nil {
t.Fatal(err)
}
}
func TestManagedCredentialAuthorizationObservesPersistedSessionWithInspection(t *testing.T) {
exitCode := 0
connection := newCredentialBrokerConnection()
connection.frames <- agentruntime.ProcessFrame{Stdout: []byte(`{"type":"disconnected"}` + "\n"), ExitCode: &exitCode}
route := &connectorRoute{
id: "account-1\x00lark-cli", connectorKey: "lark-cli", connectionID: "account-1",
releaseDigest: strings.Repeat("a", 64), credentialBrokerLaunch: &managedCredentialBrokerLaunch{timeout: 5 * time.Minute},
}
host := &credentialAuthorizationHostStub{
route: route, connections: []agentruntime.ProcessConnection{connection},
}
provider := newManagedCredentialAuthorizationProvider(host)
release := market.Release{ReleaseDigest: route.releaseDigest}
observation, err := provider.Observe(context.Background(), market.AuthorizationObserveRequest{
Scope: market.OperationScope{AccountID: "user-1"}, Connector: market.Connector{Key: "lark-cli"}, Release: release,
Session: market.AuthorizationSession{OperationID: "persisted", SessionID: "persisted/credential-broker"},
})
if err != nil {
t.Fatal(err)
}
if observation.State != market.AuthorizationObservationPending {
t.Fatalf("persisted authorization observation = %#v", observation)
}
if !reflect.DeepEqual(host.requests, []credentialBrokerRequest{{Protocol: market.CredentialBrokerProtocolV1, Operation: "inspect"}}) {
t.Fatalf("broker requests = %#v, want durable inspection", host.requests)
}
}
func TestManagedCredentialAuthorizationReturnsDeviceCodeFromBroker(t *testing.T) {
connection := newCredentialBrokerConnection()
host := &credentialAuthorizationHostStub{
route: &connectorRoute{id: "default\x00github-cli", credentialBrokerLaunch: &managedCredentialBrokerLaunch{
timeout: 5 * time.Minute, allowedHosts: map[string]struct{}{"github.com": {}},
}},
connections: []agentruntime.ProcessConnection{connection},
}
provider := newManagedCredentialAuthorizationProvider(host)
result := make(chan market.AuthorizationSession, 1)
resultErr := make(chan error, 1)
go func() {
session, err := provider.Begin(context.Background(), market.AuthorizationStartRequest{
OperationID: "authorize-github", Connector: market.Connector{Key: "github-cli"},
AfterStepRevision: 4, StepRevisionBase: 4,
})
result <- session
resultErr <- err
}()
connection.frames <- agentruntime.ProcessFrame{Stdout: []byte(`{"type":"authorization_url","url":"https://github.com/login/device","code":"ABCD-EFGH"}` + "\n")}
if err := <-resultErr; err != nil {
t.Fatal(err)
}
session := <-result
if session.AuthorizationURL != "https://github.com/login/device" || session.UserCode != "ABCD-EFGH" || session.StepRevision != 5 {
t.Fatalf("authorization session = %#v", session)
}
}
func TestManagedCredentialAuthorizationDisconnectUsesBrokerProtocol(t *testing.T) {
exitCode := 0
connection := newCredentialBrokerConnection()
connection.frames <- agentruntime.ProcessFrame{Stdout: []byte(`{"type":"disconnected"}` + "\n"), ExitCode: &exitCode}
host := &credentialAuthorizationHostStub{
route: &connectorRoute{id: "default\x00lark-cli", credentialBrokerLaunch: &managedCredentialBrokerLaunch{timeout: 5 * time.Minute}},
connections: []agentruntime.ProcessConnection{connection},
}
provider := newManagedCredentialAuthorizationProvider(host)
err := provider.Disconnect(context.Background(), market.AuthorizationDisconnectRequest{Connector: market.Connector{Key: "lark-cli"}})
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(host.requests, []credentialBrokerRequest{{Protocol: market.CredentialBrokerProtocolV1, Operation: "disconnect"}}) {
t.Fatalf("broker requests = %#v", host.requests)
}
observed := awaitAuthorizationObservations(t, host, 1)
if !reflect.DeepEqual(observed, []market.AuthorizationState{market.AuthorizationStateDisconnected}) {
t.Fatalf("authorization observations = %#v", observed)
}
}
func TestManagedCredentialAuthorizationInspectReturnsFencedObservation(t *testing.T) {
exitCode := 0
connection := newCredentialBrokerConnection()
connection.frames <- agentruntime.ProcessFrame{Stdout: []byte(`{"type":"expired","code":"token_expired","message":"login expired"}` + "\n"), ExitCode: &exitCode}
host := &credentialAuthorizationHostStub{
route: &connectorRoute{id: "account-1\x00lark-cli", connectorKey: "lark-cli", connectionID: "account-1",
releaseDigest: strings.Repeat("a", 64), credentialBrokerLaunch: &managedCredentialBrokerLaunch{timeout: 5 * time.Minute}},
connections: []agentruntime.ProcessConnection{connection},
}
provider := newManagedCredentialAuthorizationProvider(host)
connector := market.Connector{Key: "lark-cli", Release: market.Release{ReleaseDigest: strings.Repeat("a", 64)}}
observation, err := provider.Inspect(context.Background(), market.AuthorizationInspectRequest{
Scope: market.OperationScope{AccountID: "user-1"}, Connector: connector,
AccountGeneration: 3, VMAssignmentID: "vm-1", AuthorizationSessionID: "auth-1",
AuthorizationGeneration: 4, DesktopBootEpoch: "desktop-1", GuestBootID: "guest-1",
RuntimeEpoch: "runtime-1", StateRevision: 9,
})
if err != nil {
t.Fatal(err)
}
if observation.State != market.AuthorizationObservationExpired || observation.FailureCode != "token_expired" ||
observation.AccountID != "user-1" || observation.AccountGeneration != 3 || observation.VMAssignmentID != "vm-1" ||
observation.ConnectorKey != "lark-cli" || observation.ConnectionID != "account-1" ||
observation.ReleaseDigest != strings.Repeat("a", 64) || observation.StateRevision != 9 || observation.ObservedAt.IsZero() {
t.Fatalf("observation = %#v", observation)
}
if !reflect.DeepEqual(host.requests, []credentialBrokerRequest{{Protocol: market.CredentialBrokerProtocolV1, Operation: "inspect"}}) {
t.Fatalf("broker requests = %#v", host.requests)
}
}
func TestInstallationTargetsReleaseAcceptsOnlyActiveCurrentOrCandidate(t *testing.T) {
tests := []struct {
name string
installation market.Installation
releaseDigest string
want bool
}{
{
name: "installed current release",
installation: market.Installation{
State: market.InstallationStateInstalled, InstalledReleaseDigest: "current",
},
releaseDigest: "current",
want: true,
},
{
name: "installing candidate release",
installation: market.Installation{
State: market.InstallationStateInstalling, CandidateReleaseDigest: "candidate",
},
releaseDigest: "candidate",
want: true,
},
{
name: "updating candidate release",
installation: market.Installation{
State: market.InstallationStateUpdating, InstalledReleaseDigest: "current", CandidateReleaseDigest: "candidate",
},
releaseDigest: "candidate",
want: true,
},
{
name: "updating superseded current release",
installation: market.Installation{
State: market.InstallationStateUpdating, InstalledReleaseDigest: "current", CandidateReleaseDigest: "candidate",
},
releaseDigest: "current",
},
{
name: "failed candidate release",
installation: market.Installation{
State: market.InstallationStateFailed, CandidateReleaseDigest: "candidate",
},
releaseDigest: "candidate",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if got := installationTargetsRelease(test.installation, test.releaseDigest); got != test.want {
t.Fatalf("installationTargetsRelease() = %v, want %v", got, test.want)
}
})
}
}
func awaitAuthorizationObservations(t *testing.T, host *credentialAuthorizationHostStub, count int) []market.AuthorizationState {
t.Helper()
deadline := time.Now().Add(time.Second)
for {
observed := host.authorizationObservations()
if len(observed) >= count {
return observed
}
if time.Now().After(deadline) {
t.Fatalf("authorization observations = %#v, want at least %d", observed, count)
}
time.Sleep(time.Millisecond)
}
}
func TestManagedCredentialAuthorizationSharesOneBrokerAcrossConcurrentBegins(t *testing.T) {
connection := newCredentialBrokerConnection()
host := &credentialAuthorizationHostStub{
route: &connectorRoute{id: "default\x00example", credentialBrokerLaunch: &managedCredentialBrokerLaunch{
timeout: 5 * time.Minute, allowedHosts: map[string]struct{}{"accounts.example.com": {}},
}},
connections: []agentruntime.ProcessConnection{connection},
}
provider := newManagedCredentialAuthorizationProvider(host)
results := make(chan market.AuthorizationSession, 2)
errors := make(chan error, 2)
for index := 0; index < 2; index++ {
go func() {
session, err := provider.Begin(context.Background(), market.AuthorizationStartRequest{
OperationID: "authorize", Connector: market.Connector{Key: "example"},
})
results <- session
errors <- err
}()
}
connection.frames <- agentruntime.ProcessFrame{Stdout: []byte(`{"type":"authorization_url","url":"https://accounts.example.com/device"}` + "\n")}
for index := 0; index < 2; index++ {
if err := <-errors; err != nil {
t.Fatal(err)
}
if result := <-results; result.AuthorizationURL != "https://accounts.example.com/device" {
t.Fatalf("authorization session = %#v", result)
}
}
if len(host.requests) != 1 {
t.Fatalf("credential broker starts = %d, want 1", len(host.requests))
}
}
func TestManagedCredentialAuthorizationRestartsFailedBrokerOnFirstRetry(t *testing.T) {
failedConnection := newCredentialBrokerConnection()
retryConnection := newCredentialBrokerConnection()
route := &connectorRoute{id: "default\x00example", credentialBrokerLaunch: &managedCredentialBrokerLaunch{
timeout: 5 * time.Minute, allowedHosts: map[string]struct{}{"accounts.example.com": {}},
}}
host := &credentialAuthorizationHostStub{
route: route, connections: []agentruntime.ProcessConnection{failedConnection, retryConnection},
}
provider := newManagedCredentialAuthorizationProvider(host)
firstRequest := market.AuthorizationStartRequest{OperationID: "authorize-first", Connector: market.Connector{Key: "example"}}
firstResult := make(chan market.AuthorizationSession, 1)
firstError := make(chan error, 1)
go func() {
session, err := provider.Begin(context.Background(), firstRequest)
firstResult <- session
firstError <- err
}()
failedConnection.frames <- agentruntime.ProcessFrame{Stdout: []byte(`{"type":"authorization_url","url":"https://accounts.example.com/first"}` + "\n")}
if err := <-firstError; err != nil {
t.Fatal(err)
}
if result := <-firstResult; result.AuthorizationURL != "https://accounts.example.com/first" {
t.Fatalf("first authorization session = %#v", result)
}
exitCode := 1
failedConnection.frames <- agentruntime.ProcessFrame{Stderr: []byte("broker failed"), ExitCode: &exitCode}
awaitCachedAuthorizationFailure(t, provider, firstRequest.OperationID)
retryConnection.frames <- agentruntime.ProcessFrame{Stdout: []byte(`{"type":"authorization_url","url":"https://accounts.example.com/retry"}` + "\n")}
retry, err := provider.Begin(context.Background(), market.AuthorizationStartRequest{
OperationID: "authorize-retry", Connector: market.Connector{Key: "example"},
})
if err != nil {
t.Fatal(err)
}
if retry.AuthorizationURL != "https://accounts.example.com/retry" {
t.Fatalf("retry authorization session = %#v", retry)
}
if len(host.requests) != 2 {
t.Fatalf("credential broker starts = %d, want 2", len(host.requests))
}
}
func TestManagedCredentialAuthorizationCancelTerminatesAndWaitsForBrokerExit(t *testing.T) {
connection := newCredentialBrokerConnection()
route := &connectorRoute{id: "default\x00dingtalk-cli", credentialBrokerLaunch: &managedCredentialBrokerLaunch{
timeout: 5 * time.Minute, allowedHosts: map[string]struct{}{"login.dingtalk.com": {}},
}}
host := &credentialAuthorizationHostStub{route: route, connections: []agentruntime.ProcessConnection{connection}}
provider := newManagedCredentialAuthorizationProvider(host)
request := market.AuthorizationStartRequest{OperationID: "authorize-a", Connector: market.Connector{Key: "dingtalk-cli"}}
beginDone := make(chan error, 1)
go func() {
_, err := provider.Begin(context.Background(), request)
beginDone <- err
}()
connection.frames <- agentruntime.ProcessFrame{Stdout: []byte(`{"type":"authorization_url","url":"https://login.dingtalk.com/oauth"}` + "\n")}
if err := <-beginDone; err != nil {
t.Fatal(err)
}
cancelDone := make(chan error, 1)
go func() {
cancelDone <- provider.Cancel(context.Background(), market.AuthorizationCancelRequest{OperationID: request.OperationID})
}()
select {
case <-connection.closed:
case <-time.After(time.Second):
t.Fatal("cancel did not close the credential broker connection")
}
select {
case err := <-cancelDone:
if err != nil {
t.Fatal(err)
}
case <-time.After(time.Second):
t.Fatal("cancel did not wait for credential broker termination")
}
provider.mu.Lock()
if provider.sessions[request.OperationID] != nil || provider.activeByRoute[route.id] != "" {
t.Fatalf("canceled session remained active: sessions=%#v routes=%#v", provider.sessions, provider.activeByRoute)
}
provider.mu.Unlock()
if observed := host.authorizationObservations(); !reflect.DeepEqual(observed, []market.AuthorizationState{market.AuthorizationStatePending}) {
t.Fatalf("authorization observations after cancel = %#v", observed)
}
}
func awaitCachedAuthorizationFailure(t *testing.T, provider *managedCredentialAuthorizationProvider, operationID string) {
t.Helper()
deadline := time.Now().Add(time.Second)
for {
provider.mu.Lock()
session := provider.sessions[operationID]
provider.mu.Unlock()
if session != nil {
_, _, _, _, err := session.snapshot()
if err != nil {
return
}
}
if time.Now().After(deadline) {
t.Fatal("credential broker session did not fail")
}
time.Sleep(time.Millisecond)
}
}
func TestManagedCredentialAuthorizationRejectsUntrustedURL(t *testing.T) {
connection := newCredentialBrokerConnection()
host := &credentialAuthorizationHostStub{
route: &connectorRoute{id: "default\x00example", credentialBrokerLaunch: &managedCredentialBrokerLaunch{
timeout: 5 * time.Minute, allowedHosts: map[string]struct{}{"accounts.example.com": {}},
}},
connections: []agentruntime.ProcessConnection{connection},
}
provider := newManagedCredentialAuthorizationProvider(host)
result := make(chan error, 1)
go func() {
_, err := provider.Begin(context.Background(), market.AuthorizationStartRequest{
OperationID: "authorize-unsafe", Connector: market.Connector{Key: "example"},
})
result <- err
}()
connection.frames <- agentruntime.ProcessFrame{Stdout: []byte(`{"type":"authorization_url","url":"https://accounts.example.com.attacker.test/login"}` + "\n")}
if err := <-result; err == nil {
t.Fatal("untrusted authorization URL was accepted")
}
}
func awaitAuthorizationSession(
t *testing.T,
provider *managedCredentialAuthorizationProvider,
request market.AuthorizationStartRequest,
wantedURL string,
) market.AuthorizationSession {
t.Helper()
deadline := time.Now().Add(time.Second)
for {
session, err := provider.Begin(context.Background(), request)
if err != nil {
t.Fatal(err)
}
if session.AuthorizationURL == wantedURL &&
(wantedURL != "" || session.State == market.AuthorizationStateConnected) {
return session
}
if time.Now().After(deadline) {
t.Fatalf("authorization session did not reach URL %q: %#v", wantedURL, session)
}
time.Sleep(time.Millisecond)
}
}
func TestSafeCredentialBrokerURLRequiresExactHTTPSHost(t *testing.T) {
allowed := map[string]struct{}{"accounts.feishu.cn": {}}
if !safeCredentialBrokerURL("https://accounts.feishu.cn/device", allowed) {
t.Fatal("allowed credential broker URL was rejected")
}
for _, value := range []string{
"http://accounts.feishu.cn/device",
"https://accounts.feishu.cn.attacker.test/device",
"https://user@accounts.feishu.cn/device",
"https://accounts.feishu.cn:444/device",
} {
if safeCredentialBrokerURL(value, allowed) {
t.Fatalf("unsafe credential broker URL was accepted: %s", value)
}
}
}
@@ -0,0 +1,843 @@
// 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)
@@ -0,0 +1,36 @@
package implementationhost
import (
"testing"
"time"
)
func TestConnectorLifecycleLanesSerializePerConnectorAndAllowCrossConnectorConcurrency(t *testing.T) {
host := &Host{}
releaseA := host.enterConnectorLane("alpha")
bAcquired := make(chan func(), 1)
go func() { bAcquired <- host.enterConnectorLane("beta") }()
select {
case releaseB := <-bAcquired:
releaseB()
case <-time.After(time.Second):
t.Fatal("unrelated connector lane was globally serialized")
}
secondA := make(chan func(), 1)
go func() { secondA <- host.enterConnectorLane("alpha") }()
select {
case release := <-secondA:
release()
t.Fatal("same connector lane was entered concurrently")
case <-time.After(25 * time.Millisecond):
}
releaseA()
select {
case release := <-secondA:
release()
case <-time.After(time.Second):
t.Fatal("same connector lane did not resume after release")
}
}
@@ -0,0 +1,43 @@
package implementationhost
import (
"errors"
"net/http"
market "github.com/tutti-os/tutti/packages/connector/daemon/core"
"github.com/tutti-os/tutti/packages/connector/runtime/mcp"
)
const (
mcpAuthorizationRequiredCode = -33001
mcpAuthorizationExpiredCode = -33002
)
func wrapRemoteMCPAuthorizationError(err error) error {
if err == nil {
return nil
}
var domain *market.DomainError
if errors.As(err, &domain) && domain.Code == market.ErrorCodeAuthorizationFailed {
return err
}
if !remoteMCPAuthorizationRequired(err) {
return err
}
return market.NewDomainError(
market.ErrorCodeAuthorizationFailed,
"connector authorization is required",
false,
err,
)
}
func remoteMCPAuthorizationRequired(err error) bool {
var rpcErr *mcp.RPCError
if errors.As(err, &rpcErr) &&
(rpcErr.Code == mcpAuthorizationRequiredCode || rpcErr.Code == mcpAuthorizationExpiredCode) {
return true
}
var httpErr *mcp.ModernHTTPError
return errors.As(err, &httpErr) && httpErr.StatusCode == http.StatusPreconditionRequired
}
@@ -0,0 +1,31 @@
package implementationhost
import (
"errors"
"net/http"
"testing"
market "github.com/tutti-os/tutti/packages/connector/daemon/core"
"github.com/tutti-os/tutti/packages/connector/runtime/mcp"
)
func TestWrapRemoteMCPAuthorizationErrorMapsPreconditionRequired(t *testing.T) {
cause := &mcp.ModernHTTPError{
StatusCode: http.StatusPreconditionRequired,
Cause: &mcp.RPCError{Code: -33001, Message: "authorization required"},
}
err := wrapRemoteMCPAuthorizationError(cause)
var domain *market.DomainError
if !errors.As(err, &domain) || domain.Code != market.ErrorCodeAuthorizationFailed || domain.Retryable {
t.Fatalf("error = %#v", err)
}
}
func TestWrapRemoteMCPAuthorizationErrorLeavesTransientFailures(t *testing.T) {
cause := &mcp.ModernHTTPError{StatusCode: http.StatusBadGateway}
err := wrapRemoteMCPAuthorizationError(cause)
var domain *market.DomainError
if err != cause || errors.As(err, &domain) {
t.Fatalf("error = %#v", err)
}
}
@@ -0,0 +1,374 @@
package implementationhost
import (
"context"
"encoding/json"
"errors"
"fmt"
"log/slog"
"sort"
"strings"
"sync"
connectorruntime "github.com/tutti-os/tutti/packages/connector/runtime"
connectormcp "github.com/tutti-os/tutti/packages/connector/runtime/mcp"
)
// MCPTool is the connector-scoped projection exposed by the local connector
// MCP server. UpstreamName is deliberately not serialized: callers use the
// namespaced local name and the registry resolves the upstream binding.
type MCPTool struct {
Name string `json:"name"`
Description string `json:"description,omitempty"`
InputSchema map[string]any `json:"inputSchema"`
// ConnectorKey and ConnectorVersion are trusted in-process route provenance.
// They are never exposed through the MCP tools/list JSON contract or accepted
// from Agent input.
ConnectorKey string `json:"-"`
ConnectorVersion string `json:"-"`
}
// MCPToolProjection derives the exact transport-validation contract for a
// resolved live Tool. A Session router can use this to inject an
// authority-specific discriminator after it has selected Owner or Caller,
// without asking the transport validator to interpret a presentation oneOf.
type MCPToolProjection func(MCPTool) (MCPTool, error)
type registeredMCPTool struct {
routeID string
localName string
upstreamName string
description string
inputSchema map[string]any
client mcpCaller
}
// MCPRegistry exposes only Connector-owned MCP routes. It intentionally
// shares the implementation host's generation-fenced RouteTable so MCP and
// CLI publication observe the same lifecycle boundary.
type MCPRegistry struct {
mu sync.RWMutex
routes *connectorruntime.RouteTable
subscribers map[uint64]chan struct{}
nextID uint64
authorizationErrorObserver func()
}
func NewMCPRegistry() *MCPRegistry {
return &MCPRegistry{subscribers: make(map[uint64]chan struct{})}
}
func (registry *MCPRegistry) attach(routes *connectorruntime.RouteTable) {
registry.mu.Lock()
registry.routes = routes
registry.mu.Unlock()
}
func (registry *MCPRegistry) activeRoutes() []*connectorRoute {
if registry == nil {
return nil
}
registry.mu.RLock()
table := registry.routes
registry.mu.RUnlock()
if table == nil {
return nil
}
portable := table.PublishedRoutes()
routes := make([]*connectorRoute, 0, len(portable))
for _, candidate := range portable {
if route, ok := candidate.(*connectorRoute); ok && len(route.mcpTools) > 0 {
routes = append(routes, route)
}
}
return routes
}
// Tools reads each active downstream MCP directly. Route activation keeps only
// bootstrap evidence; it is not an Agent-visible tool-list cache.
func (registry *MCPRegistry) Tools(ctx context.Context) ([]MCPTool, error) {
routes := registry.activeRoutes()
type routeToolsResult struct {
route *connectorRoute
tools []mcpTool
err error
}
results := make(chan routeToolsResult, len(routes))
for _, route := range routes {
go func(route *connectorRoute) {
tools, err := registry.listRouteTools(ctx, route)
results <- routeToolsResult{route: route, tools: tools, err: err}
}(route)
}
seen := make(map[string]struct{})
result := make([]MCPTool, 0)
var listErr error
succeeded := 0
for range routes {
listed := <-results
if listed.err != nil {
listErr = errors.Join(listErr, fmt.Errorf("%s: %w", listed.route.connectorKey, listed.err))
slog.Warn("connector MCP tools/list omitted unavailable route", "connectorKey", listed.route.connectorKey, "error", listed.err)
continue
}
succeeded++
for _, tool := range listed.tools {
localName := listed.route.connectorKey + "_" + tool.Name
if _, duplicate := seen[localName]; duplicate {
continue
}
seen[localName] = struct{}{}
result = append(result, MCPTool{Name: localName, Description: tool.Description,
InputSchema: cloneJSONMap(tool.InputSchema), ConnectorKey: listed.route.connectorKey,
ConnectorVersion: listed.route.connectorVersion})
}
}
if len(routes) > 0 && succeeded == 0 {
return nil, listErr
}
sort.Slice(result, func(left, right int) bool { return result[left].Name < result[right].Name })
return result, nil
}
// Call resolves a native namespaced tool against an immutable current route.
// Duplicate bindings fail closed instead of selecting an arbitrary account or
// generation.
func (registry *MCPRegistry) Call(ctx context.Context, name string, arguments map[string]any) (json.RawMessage, error) {
return registry.CallValidated(ctx, name, arguments, nil)
}
// CallValidated resolves the current downstream Tool once, lets the local
// transport validate request-integrity headers against that exact live schema,
// and then calls the same binding.
func (registry *MCPRegistry) CallValidated(ctx context.Context, name string, arguments map[string]any, validate func(MCPTool) error) (json.RawMessage, error) {
return registry.CallProjectedValidated(ctx, name, arguments, nil, validate)
}
// CallProjectedValidated resolves the current downstream Tool once, projects
// an authority-specific validation schema, validates that projected contract,
// and calls the same immutable live binding. Projection cannot rename a Tool,
// change its Connector provenance, or remove its input schema.
func (registry *MCPRegistry) CallProjectedValidated(
ctx context.Context,
name string,
arguments map[string]any,
project MCPToolProjection,
validate func(MCPTool) error,
) (json.RawMessage, error) {
name = strings.TrimSpace(name)
if name == "" {
return nil, errors.New("connector MCP tool was not found")
}
type candidateResult struct {
route *connectorRoute
tools []mcpTool
err error
}
candidates := make([]*connectorRoute, 0)
for _, route := range registry.activeRoutes() {
if strings.HasPrefix(name, route.connectorKey+"_") {
candidates = append(candidates, route)
}
}
results := make(chan candidateResult, len(candidates))
for _, route := range candidates {
go func(route *connectorRoute) {
tools, err := registry.listRouteTools(ctx, route)
results <- candidateResult{route: route, tools: tools, err: err}
}(route)
}
type resolvedBinding struct {
binding registeredMCPTool
owner *connectorRoute
tool MCPTool
}
bindings := make([]resolvedBinding, 0, 1)
var resolveErr error
for range candidates {
listed := <-results
if listed.err != nil {
resolveErr = errors.Join(resolveErr, fmt.Errorf("%s: %w", listed.route.connectorKey, listed.err))
continue
}
for _, tool := range listed.tools {
localName := listed.route.connectorKey + "_" + tool.Name
if localName != name {
continue
}
bindings = append(bindings, resolvedBinding{
binding: registeredMCPTool{localName: localName, upstreamName: tool.Name, client: routeMCPCaller(listed.route)}, owner: listed.route,
tool: MCPTool{Name: localName, Description: tool.Description, InputSchema: cloneJSONMap(tool.InputSchema),
ConnectorKey: listed.route.connectorKey, ConnectorVersion: listed.route.connectorVersion},
})
}
}
if len(bindings) == 0 {
if resolveErr != nil {
return nil, resolveErr
}
return nil, errors.New("connector MCP tool was not found")
}
if len(bindings) != 1 {
return nil, errors.New("connector MCP tool binding is ambiguous")
}
resolved := bindings[0]
validationTool := resolved.tool
if project != nil {
projected, err := project(validationTool)
if err != nil {
return nil, err
}
if strings.TrimSpace(projected.Name) != validationTool.Name || projected.InputSchema == nil ||
strings.TrimSpace(projected.ConnectorKey) != validationTool.ConnectorKey ||
strings.TrimSpace(projected.ConnectorVersion) != validationTool.ConnectorVersion {
return nil, errors.New("connector MCP projected tool contract is invalid")
}
validationTool = projected
}
if validate != nil {
if err := validate(validationTool); err != nil {
return nil, err
}
}
if !registry.routeCurrent(resolved.owner) {
return nil, errors.New("connector MCP route is no longer active")
}
result, err := resolved.binding.client.Call(ctx, "tools/call", map[string]any{
"name": resolved.binding.upstreamName, "arguments": arguments,
})
if err != nil {
registry.observeAuthorizationError(err)
}
return result, err
}
func (registry *MCPRegistry) listRouteTools(ctx context.Context, route *connectorRoute) ([]mcpTool, error) {
client := routeMCPCaller(route)
if client == nil {
return nil, errors.New("connector MCP route is unavailable")
}
var tools []mcpTool
var err error
if route.remoteMCP != nil {
tools, err = listModernMCPTools(ctx, client)
} else {
tools, err = listMCPTools(ctx, client)
}
if err != nil {
registry.observeAuthorizationError(err)
return nil, err
}
if err := validateMCPToolContracts(route.connectorKey, tools); err != nil {
return nil, err
}
if route.remoteMCP != nil {
schemas := make(map[string]map[string]any, len(tools))
for _, tool := range tools {
schemas[tool.Name] = tool.InputSchema
}
if err := route.remoteMCP.ReplaceTools(schemas); err != nil {
return nil, err
}
}
return tools, nil
}
func routeMCPCaller(route *connectorRoute) mcpCaller {
if route == nil {
return nil
}
if route.remoteMCP != nil {
return route.remoteMCP
}
if route.mcpClient != nil {
return route.mcpClient
}
for _, tool := range route.mcpTools {
return tool.client
}
return nil
}
func (registry *MCPRegistry) observeAuthorizationError(err error) {
var rpcErr *connectormcp.RPCError
if !errors.As(err, &rpcErr) ||
(rpcErr.Code != mcpAuthorizationRequiredCode && rpcErr.Code != mcpAuthorizationExpiredCode) {
return
}
registry.mu.RLock()
observer := registry.authorizationErrorObserver
registry.mu.RUnlock()
if observer != nil {
observer()
}
}
func (registry *MCPRegistry) SetAuthorizationErrorObserver(observer func()) {
if registry == nil {
return
}
registry.mu.Lock()
registry.authorizationErrorObserver = observer
registry.mu.Unlock()
}
func (registry *MCPRegistry) routeCurrent(route *connectorRoute) bool {
registry.mu.RLock()
table := registry.routes
registry.mu.RUnlock()
return table != nil && table.IsCurrent(route)
}
// Subscribe is used by the local MCP server to fan out
// notifications/tools/list_changed. Notifications are edge-triggered and
// coalesced for slow consumers.
func (registry *MCPRegistry) Subscribe() (<-chan struct{}, func()) {
if registry == nil {
closed := make(chan struct{})
close(closed)
return closed, func() {}
}
registry.mu.Lock()
registry.nextID++
id := registry.nextID
updates := make(chan struct{}, 1)
registry.subscribers[id] = updates
registry.mu.Unlock()
return updates, func() {
registry.mu.Lock()
if current, ok := registry.subscribers[id]; ok {
delete(registry.subscribers, id)
close(current)
}
registry.mu.Unlock()
}
}
func (registry *MCPRegistry) notifyChanged() {
if registry == nil {
return
}
registry.mu.RLock()
defer registry.mu.RUnlock()
for _, updates := range registry.subscribers {
select {
case updates <- struct{}{}:
default:
}
}
}
func cloneJSONMap(input map[string]any) map[string]any {
if input == nil {
return nil
}
raw, err := json.Marshal(input)
if err != nil {
return nil
}
var result map[string]any
if json.Unmarshal(raw, &result) != nil {
return nil
}
return result
}
@@ -0,0 +1,260 @@
package implementationhost
import (
"context"
"encoding/json"
"errors"
"strings"
"testing"
"time"
market "github.com/tutti-os/tutti/packages/connector/daemon/core"
connectorruntime "github.com/tutti-os/tutti/packages/connector/runtime"
)
type registryMCPCaller struct {
method string
params map[string]any
}
type failingListCaller struct{}
func (failingListCaller) Call(_ context.Context, method string, _ any) (json.RawMessage, error) {
if method == "tools/list" {
return nil, errors.New("unrelated connector unavailable")
}
return nil, errors.New("unexpected call")
}
func (caller *registryMCPCaller) Call(_ context.Context, method string, params any) (json.RawMessage, error) {
caller.method = method
caller.params, _ = params.(map[string]any)
if method == "tools/list" {
return json.RawMessage(`{"tools":[{"name":"status","description":"Read status","inputSchema":{"type":"object","oneOf":[{"required":["id"]}]}}]}`), nil
}
return json.RawMessage(`{"content":[{"type":"text","text":"ready"}]}`), nil
}
func TestMCPRegistryListsCallsAndNotifies(t *testing.T) {
table := connectorruntime.NewRouteTable()
registry := NewMCPRegistry()
registry.attach(table)
caller := &registryMCPCaller{}
route := &connectorRoute{id: connectorRouteKey("default", "github"), connectionID: "default", connectorKey: "github", connectorVersion: "2.0.0",
releaseDigest: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
generation: market.HostGeneration{BootEpoch: "boot", Generation: 1}, processes: connectorruntime.NewProcessGroup(),
mcpTools: map[string]registeredMCPTool{
"github_status": {routeID: "connector.github.mcp.status", localName: "github_status", upstreamName: "status",
description: "Read status", inputSchema: map[string]any{"type": "object", "oneOf": []any{map[string]any{"required": []any{"id"}}}}, client: caller},
}}
if err := table.Commit(route); err != nil {
t.Fatal(err)
}
tools, err := registry.Tools(context.Background())
if err != nil {
t.Fatal(err)
}
if len(tools) != 1 || tools[0].Name != "github_status" || tools[0].InputSchema["type"] != "object" ||
tools[0].ConnectorKey != "github" || tools[0].ConnectorVersion != "2.0.0" {
t.Fatalf("tools = %#v", tools)
}
if _, preserved := tools[0].InputSchema["oneOf"]; !preserved {
t.Fatalf("native MCP JSON Schema was narrowed: %#v", tools[0].InputSchema)
}
encoded, err := json.Marshal(tools[0])
if err != nil || strings.Contains(string(encoded), "ConnectorKey") || strings.Contains(string(encoded), "connectorKey") ||
strings.Contains(string(encoded), "ConnectorVersion") || strings.Contains(string(encoded), "connectorVersion") {
t.Fatalf("trusted Connector provenance leaked into MCP JSON: %s, %v", encoded, err)
}
raw, err := registry.Call(context.Background(), "github_status", map[string]any{"verbose": true})
if err != nil || len(raw) == 0 || caller.method != "tools/call" || caller.params["name"] != "status" {
t.Fatalf("call raw=%s method=%q params=%#v err=%v", raw, caller.method, caller.params, err)
}
updates, unsubscribe := registry.Subscribe()
defer unsubscribe()
registry.notifyChanged()
select {
case <-updates:
case <-time.After(time.Second):
t.Fatal("registry notification was not delivered")
}
if err := table.Remove(route.id, route.generation, route.releaseDigest, time.Now().Add(time.Second)); err != nil {
t.Fatal(err)
}
if tools, err := registry.Tools(context.Background()); err != nil || len(tools) != 0 {
t.Fatalf("retired tools = %#v", tools)
}
}
func TestRegisterMCPToolsKeepsNativeNamesAndSchemasOutsideLegacyCommandSubset(t *testing.T) {
route := &connectorRoute{connectorKey: "lark-cli", mcpTools: make(map[string]registeredMCPTool)}
caller := &registryMCPCaller{}
err := (&Host{}).registerMCPTools(route, caller, []mcpTool{{
Name: "Read:Item", InputSchema: map[string]any{
"oneOf": []any{map[string]any{"type": "object"}, map[string]any{"type": "null"}},
},
}})
if err != nil {
t.Fatal(err)
}
registered, ok := route.mcpTools["lark-cli_Read:Item"]
if !ok || registered.upstreamName != "Read:Item" || registered.routeID != "connector.lark-cli.mcp.Read:Item" {
t.Fatalf("registered MCP tool = %#v", route.mcpTools)
}
}
func TestMCPRegistryCallDoesNotListUnrelatedConnector(t *testing.T) {
table := connectorruntime.NewRouteTable()
registry := NewMCPRegistry()
registry.attach(table)
target := &registryMCPCaller{}
routes := []*connectorRoute{
{id: connectorRouteKey("default", "slow"), connectionID: "default", connectorKey: "slow", releaseDigest: strings.Repeat("a", 64),
generation: market.HostGeneration{BootEpoch: "boot", Generation: 1}, processes: connectorruntime.NewProcessGroup(),
mcpTools: map[string]registeredMCPTool{"slow_wait": {client: failingListCaller{}}}},
{id: connectorRouteKey("default", "github"), connectionID: "default", connectorKey: "github", releaseDigest: strings.Repeat("b", 64),
generation: market.HostGeneration{BootEpoch: "boot", Generation: 1}, processes: connectorruntime.NewProcessGroup(),
mcpTools: map[string]registeredMCPTool{"github_status": {client: target}}},
}
for _, route := range routes {
if err := table.Commit(route); err != nil {
t.Fatal(err)
}
}
tools, err := registry.Tools(context.Background())
if err != nil || len(tools) != 1 || tools[0].Name != "github_status" {
t.Fatalf("partial tools = %#v, %v", tools, err)
}
if _, err := registry.Call(context.Background(), "github_status", map[string]any{}); err != nil {
t.Fatalf("target call was blocked by unrelated connector: %v", err)
}
}
func TestMCPRegistryCallIsolatesFailingOverlappingNamespaceCandidate(t *testing.T) {
table := connectorruntime.NewRouteTable()
registry := NewMCPRegistry()
registry.attach(table)
target := &registryMCPCaller{}
routes := []*connectorRoute{
{id: connectorRouteKey("default", "github"), connectionID: "default", connectorKey: "github", releaseDigest: strings.Repeat("a", 64),
generation: market.HostGeneration{BootEpoch: "boot", Generation: 1}, processes: connectorruntime.NewProcessGroup(),
mcpTools: map[string]registeredMCPTool{"github_wait": {client: failingListCaller{}}}},
{id: connectorRouteKey("default", "github_enterprise"), connectionID: "default", connectorKey: "github_enterprise", releaseDigest: strings.Repeat("b", 64),
generation: market.HostGeneration{BootEpoch: "boot", Generation: 1}, processes: connectorruntime.NewProcessGroup(),
mcpTools: map[string]registeredMCPTool{"github_enterprise_status": {client: target}}},
}
for _, route := range routes {
if err := table.Commit(route); err != nil {
t.Fatal(err)
}
}
if _, err := registry.Call(context.Background(), "github_enterprise_status", map[string]any{}); err != nil {
t.Fatalf("overlapping failed candidate blocked target call: %v", err)
}
}
func TestMCPRegistryCallProjectedValidatedUsesSelectedLiveSchemaAndBinding(t *testing.T) {
table := connectorruntime.NewRouteTable()
registry := NewMCPRegistry()
registry.attach(table)
caller := &registryMCPCaller{}
route := &connectorRoute{
id: connectorRouteKey("default", "github"), connectionID: "default", connectorKey: "github", connectorVersion: "2.0.0",
releaseDigest: strings.Repeat("a", 64), generation: market.HostGeneration{BootEpoch: "boot", Generation: 1},
processes: connectorruntime.NewProcessGroup(),
mcpTools: map[string]registeredMCPTool{"github_status": {client: caller}},
}
if err := table.Commit(route); err != nil {
t.Fatal(err)
}
projected := false
validated := false
raw, err := registry.CallProjectedValidated(
context.Background(), "github_status", map[string]any{"connectorAuthority": "owner"},
func(tool MCPTool) (MCPTool, error) {
projected = true
if tool.ConnectorKey != "github" || tool.ConnectorVersion != "2.0.0" {
t.Fatalf("projection Connector provenance = %q@%q", tool.ConnectorKey, tool.ConnectorVersion)
}
if _, ok := tool.InputSchema["oneOf"]; !ok {
t.Fatalf("projection did not receive live native schema: %#v", tool.InputSchema)
}
tool.InputSchema["properties"] = map[string]any{
"connectorAuthority": map[string]any{"type": "string", "enum": []any{"owner"}},
}
return tool, nil
},
func(tool MCPTool) error {
validated = true
properties, _ := tool.InputSchema["properties"].(map[string]any)
if properties["connectorAuthority"] == nil {
t.Fatalf("validator did not receive authority-specific schema: %#v", tool.InputSchema)
}
return nil
},
)
if err != nil || len(raw) == 0 || !projected || !validated || caller.method != "tools/call" || caller.params["name"] != "status" {
t.Fatalf("projected call raw=%s projected=%v validated=%v method=%q params=%#v err=%v", raw, projected, validated, caller.method, caller.params, err)
}
}
func TestMCPRegistryCallProjectedValidatedRejectsRenamedContractBeforeCall(t *testing.T) {
table := connectorruntime.NewRouteTable()
registry := NewMCPRegistry()
registry.attach(table)
caller := &registryMCPCaller{}
route := &connectorRoute{
id: connectorRouteKey("default", "github"), connectionID: "default", connectorKey: "github",
releaseDigest: strings.Repeat("b", 64), generation: market.HostGeneration{BootEpoch: "boot", Generation: 1},
processes: connectorruntime.NewProcessGroup(),
mcpTools: map[string]registeredMCPTool{"github_status": {client: caller}},
}
if err := table.Commit(route); err != nil {
t.Fatal(err)
}
_, err := registry.CallProjectedValidated(context.Background(), "github_status", nil, func(tool MCPTool) (MCPTool, error) {
tool.Name = "github_other"
return tool, nil
}, nil)
if err == nil || caller.method == "tools/call" {
t.Fatalf("renamed projected contract reached upstream: method=%q err=%v", caller.method, err)
}
}
func TestMCPRegistryCallProjectedValidatedRejectsChangedConnectorProvenance(t *testing.T) {
tests := []struct {
name string
mutate func(*MCPTool)
}{
{name: "key", mutate: func(tool *MCPTool) { tool.ConnectorKey = "foo" }},
{name: "version", mutate: func(tool *MCPTool) { tool.ConnectorVersion = "1.0.0" }},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
table := connectorruntime.NewRouteTable()
registry := NewMCPRegistry()
registry.attach(table)
caller := &registryMCPCaller{}
route := &connectorRoute{
id: connectorRouteKey("default", "foo_bar"), connectionID: "default",
connectorKey: "foo_bar", connectorVersion: "2.0.0",
releaseDigest: strings.Repeat("c", 64), generation: market.HostGeneration{BootEpoch: "boot", Generation: 1},
processes: connectorruntime.NewProcessGroup(),
mcpTools: map[string]registeredMCPTool{"foo_bar_status": {client: caller}},
}
if err := table.Commit(route); err != nil {
t.Fatal(err)
}
_, err := registry.CallProjectedValidated(context.Background(), "foo_bar_status", nil, func(tool MCPTool) (MCPTool, error) {
if tool.ConnectorKey != "foo_bar" || tool.ConnectorVersion != "2.0.0" {
t.Fatalf("exact route provenance = %q@%q", tool.ConnectorKey, tool.ConnectorVersion)
}
test.mutate(&tool)
return tool, nil
}, nil)
if err == nil || caller.method == "tools/call" {
t.Fatalf("changed Connector provenance reached upstream: method=%q err=%v", caller.method, err)
}
})
}
}
@@ -0,0 +1,156 @@
package implementationhost
import (
"context"
"encoding/json"
"errors"
"fmt"
"strings"
market "github.com/tutti-os/tutti/packages/connector/daemon/core"
)
func (host *Host) buildRemoteRoute(ctx context.Context, request market.RuntimeReconcileRequest) (*connectorRoute, error) {
remote := request.Connector.Release.Manifest.Implementation.RemoteStreamableHTTP
if remote == nil {
return nil, errors.New("remote_streamable_http connector config is unavailable")
}
accountID := strings.TrimSpace(request.Scope.AccountID)
if accountID == "" || host.remoteMCPClientFactory == nil {
return nil, errors.New("remote MCP product client factory is unavailable")
}
client, err := host.remoteMCPClientFactory.NewRemoteMCPClient(ctx, RemoteMCPClientRequest{
OperationID: request.OperationID, ConnectionID: request.ConnectionID,
ConnectorKey: request.Connector.Key, AccountID: accountID,
ReleaseDigest: request.Connector.Release.ReleaseDigest,
Version: request.Connector.Release.Version,
Generation: request.Generation,
Implementation: *remote,
})
if err != nil {
return nil, fmt.Errorf("create remote connector MCP client: %w", err)
}
closeClient := func() { _ = client.Close(context.Background()) }
if _, err := client.Call(ctx, "server/discover", map[string]any{}); err != nil {
closeClient()
return nil, wrapRemoteMCPAuthorizationError(fmt.Errorf("discover remote connector MCP: %w", err))
}
cacheKey := remoteMCPToolCacheKeyFrom(request)
tools, cached := host.remoteMCPTools.lookup(cacheKey)
if !cached {
var err error
tools, err = listModernMCPTools(ctx, client)
if err != nil {
closeClient()
return nil, wrapRemoteMCPAuthorizationError(err)
}
}
for _, tool := range tools {
if err := client.RegisterTool(tool.Name, tool.InputSchema); err != nil {
closeClient()
return nil, fmt.Errorf("register remote connector MCP Tool %q: %w", tool.Name, err)
}
}
route := newConnectorRoute(request)
if err := host.registerMCPTools(route, client, tools); err != nil {
closeClient()
return nil, err
}
if !cached {
host.remoteMCPTools.store(cacheKey, tools)
}
route.remoteMCP = client
return route, nil
}
type mcpCaller interface {
Call(context.Context, string, any) (json.RawMessage, error)
}
func (*Host) registerMCPTools(route *connectorRoute, client mcpCaller, tools []mcpTool) error {
if len(tools) == 0 {
return errors.New("connector MCP tools/list response is invalid")
}
if err := validateMCPToolContracts(route.connectorKey, tools); err != nil {
return err
}
for _, tool := range tools {
localName := route.connectorKey + "_" + tool.Name
// Keep the upstream JSON Schema intact. MCP schemas are not constrained
// by Tutti's legacy command-input schema subset.
routeID := "connector." + route.connectorKey + ".mcp." + tool.Name
route.mcpTools[localName] = registeredMCPTool{routeID: routeID, localName: localName,
upstreamName: tool.Name, description: tool.Description, inputSchema: cloneJSONMap(tool.InputSchema), client: client}
}
return nil
}
func validateMCPToolContracts(connectorKey string, tools []mcpTool) error {
seen := make(map[string]struct{}, len(tools))
for _, tool := range tools {
localName := connectorKey + "_" + tool.Name
if strings.TrimSpace(tool.Name) == "" || tool.InputSchema == nil || len(localName) > 255 || !mcpLocalToolNamePattern.MatchString(localName) {
return errors.New("connector MCP tool contract is invalid")
}
if _, duplicate := seen[localName]; duplicate {
return errors.New("connector MCP tool capability id is duplicated")
}
seen[localName] = struct{}{}
}
return nil
}
type mcpTool struct {
Name string `json:"name"`
Description string `json:"description"`
InputSchema map[string]any `json:"inputSchema"`
}
func listMCPTools(ctx context.Context, client mcpCaller) ([]mcpTool, error) {
return listMCPToolsWithProtocol(ctx, client, false)
}
func listModernMCPTools(ctx context.Context, client mcpCaller) ([]mcpTool, error) {
return listMCPToolsWithProtocol(ctx, client, true)
}
func listMCPToolsWithProtocol(ctx context.Context, client mcpCaller, requireComplete bool) ([]mcpTool, error) {
const maxPages = 64
const maxTools = 512
result := make([]mcpTool, 0)
var cursor *string
seen := map[string]struct{}{}
for page := 0; page < maxPages; page++ {
params := map[string]any{}
if cursor != nil {
params["cursor"] = *cursor
}
raw, err := client.Call(ctx, "tools/list", params)
if err != nil {
return nil, fmt.Errorf("list connector MCP tools: %w", err)
}
var listing struct {
ResultType string `json:"resultType"`
Tools []mcpTool `json:"tools"`
NextCursor *string `json:"nextCursor"`
}
if err := json.Unmarshal(raw, &listing); err != nil ||
(requireComplete && listing.ResultType != "" && listing.ResultType != "complete") {
return nil, errors.New("connector MCP tools/list response is invalid")
}
result = append(result, listing.Tools...)
if len(result) > maxTools {
return nil, errors.New("connector MCP tools/list exceeds tool limit")
}
if listing.NextCursor == nil {
return result, nil
}
next := *listing.NextCursor
if _, duplicate := seen[next]; duplicate {
return nil, errors.New("connector MCP tools/list cursor repeated")
}
seen[next] = struct{}{}
cursor = &next
}
return nil, errors.New("connector MCP tools/list exceeds page limit")
}
@@ -0,0 +1,36 @@
package implementationhost
import (
"context"
"encoding/json"
"testing"
)
type fixedMCPCaller struct {
result json.RawMessage
}
func (caller fixedMCPCaller) Call(context.Context, string, any) (json.RawMessage, error) {
return caller.result, nil
}
func TestListModernMCPToolsAcceptsStandardResponseWithoutResultTypeExtension(t *testing.T) {
tools, err := listModernMCPTools(context.Background(), fixedMCPCaller{result: json.RawMessage(
`{"tools":[{"name":"search","description":"Search","inputSchema":{"type":"object"}}]}`,
)})
if err != nil {
t.Fatal(err)
}
if len(tools) != 1 || tools[0].Name != "search" {
t.Fatalf("tools = %#v", tools)
}
}
func TestListModernMCPToolsRejectsExplicitIncompleteResponse(t *testing.T) {
_, err := listModernMCPTools(context.Background(), fixedMCPCaller{result: json.RawMessage(
`{"resultType":"partial","tools":[{"name":"search","inputSchema":{"type":"object"}}]}`,
)})
if err == nil {
t.Fatal("explicit incomplete tools/list response was accepted")
}
}
@@ -0,0 +1,37 @@
package implementationhost
import (
"context"
"time"
market "github.com/tutti-os/tutti/packages/connector/daemon/core"
)
// AuthorizationObserver receives runtime-side credential binding outcomes.
// The owning product may project them upstream; this Host does not become the
// account authorization truth source.
type AuthorizationObserver interface {
ObserveAuthorization(context.Context, AuthorizationObservation)
}
type AuthorizationObservation struct {
ConnectorKey string
ConnectionID string
State market.AuthorizationState
ObservedAt time.Time
}
// RouteObserver receives unexpected long-lived runtime exits after the route
// has been removed from publication. Intentional deactivate, replacement, and
// Host shutdown do not emit an observation.
type RouteObserver interface {
ObserveRoute(context.Context, RouteObservation)
}
type RouteObservation struct {
ConnectorKey string
ConnectionID string
ReleaseDigest string
Generation market.HostGeneration
ObservedAt time.Time
}
@@ -0,0 +1,72 @@
package implementationhost
import (
"context"
"errors"
"fmt"
"io"
"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"
)
const maxCLIReadinessOutput = 64 << 10
func (host *Host) checkCLIReadiness(ctx context.Context, route *connectorRoute, probe *market.CLIReadinessProbe) error {
if probe == nil {
return nil
}
if host == nil || route == nil || route.cliLaunch == nil {
return errors.New("connector CLI readiness target is unavailable")
}
launch := route.cliLaunch
probeContext, cancel := context.WithTimeout(ctx, time.Duration(probe.TimeoutMS)*time.Millisecond)
defer cancel()
arguments := append(append([]string(nil), launch.arguments...), probe.Arguments...)
spec := connectorruntime.ConnectorProcessSpec(route.connectionID, route.connectorKey, launch.language,
launch.executable, launch.cwd, arguments, launch.stateDir, route.userHome, launch.artifactTrees)
connection, processID, err := host.startProcess(probeContext, route, spec, false)
if err != nil {
return err
}
defer func() { _ = route.releaseProcess(processID, connection) }()
if graceful, ok := connection.(agentruntime.GracefulProcessConnection); ok {
_ = graceful.CloseInput()
}
return waitCLIReadiness(probeContext, connection)
}
func waitCLIReadiness(ctx context.Context, connection agentruntime.ProcessConnection) error {
outputBytes := 0
for {
var frame agentruntime.ProcessFrame
var err error
if contextual, ok := connection.(agentruntime.ContextProcessConnection); ok {
frame, err = contextual.RecvContext(ctx)
} else {
frame, err = connection.Recv()
}
if err != nil {
if errors.Is(err, io.EOF) {
return errors.New("CLI readiness probe exited without an exit code")
}
return err
}
outputBytes += len(frame.Stdout) + len(frame.Stderr)
if outputBytes > maxCLIReadinessOutput {
if graceful, ok := connection.(agentruntime.GracefulProcessConnection); ok {
_ = graceful.Kill()
}
return errors.New("CLI readiness probe output exceeded its limit")
}
if frame.ExitCode == nil {
continue
}
if *frame.ExitCode != 0 {
return fmt.Errorf("CLI readiness probe exited with code %d", *frame.ExitCode)
}
return nil
}
}
@@ -0,0 +1,212 @@
package implementationhost
import (
"context"
"encoding/json"
"errors"
"net/http"
"reflect"
"testing"
market "github.com/tutti-os/tutti/packages/connector/daemon/core"
"github.com/tutti-os/tutti/packages/connector/runtime/mcp"
)
type remoteMCPFactoryRecorder struct {
request RemoteMCPClientRequest
client RemoteMCPClient
}
func (factory *remoteMCPFactoryRecorder) NewRemoteMCPClient(_ context.Context, request RemoteMCPClientRequest) (RemoteMCPClient, error) {
factory.request = request
return factory.client, nil
}
type remoteMCPClientStub struct {
calls []string
registered []string
closed bool
listErr error
}
func (client *remoteMCPClientStub) Call(_ context.Context, method string, _ any) (json.RawMessage, error) {
client.calls = append(client.calls, method)
if method == "tools/list" {
if client.listErr != nil {
return nil, client.listErr
}
return json.RawMessage(`{"resultType":"complete","tools":[{"name":"search","description":"Search","inputSchema":{"type":"object"}}]}`), nil
}
return json.RawMessage(`{"resultType":"complete"}`), nil
}
func (client *remoteMCPClientStub) RegisterTool(name string, _ map[string]any) error {
client.registered = append(client.registered, name)
return nil
}
func (*remoteMCPClientStub) ReplaceTools(map[string]map[string]any) error { return nil }
func (client *remoteMCPClientStub) Close(context.Context) error {
client.closed = true
return nil
}
func TestBuildRemoteRouteUsesProductClientFactoryWithLifecycleIdentity(t *testing.T) {
client := &remoteMCPClientStub{}
factory := &remoteMCPFactoryRecorder{client: client}
host := &Host{remoteMCPClientFactory: factory}
implementation := market.RemoteStreamableHTTPImplementation{
ProtocolVersion: mcp.ModernProtocolVersion, BindingRef: "documents.primary", ContractVersion: 1,
BindingContractHash: "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
}
generation := market.HostGeneration{BootEpoch: "boot-1", Generation: 7}
request := market.RuntimeReconcileRequest{
OperationID: "operation-1", ConnectionID: "connection-1", Scope: market.OperationScope{AccountID: "account-1"},
Connector: market.Connector{Key: "documents", Release: market.Release{
Version: "1.2.3", ReleaseDigest: "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd",
Manifest: market.Manifest{Implementation: market.Implementation{
Kind: market.ImplementationKindRemoteStreamableHTTP, RemoteStreamableHTTP: &implementation,
}},
}},
Generation: generation,
}
route, err := host.buildRemoteRoute(context.Background(), request)
if err != nil {
t.Fatal(err)
}
want := RemoteMCPClientRequest{
OperationID: "operation-1", ConnectionID: "connection-1", ConnectorKey: "documents", AccountID: "account-1",
ReleaseDigest: request.Connector.Release.ReleaseDigest, Version: "1.2.3", Generation: generation, Implementation: implementation,
}
if !reflect.DeepEqual(factory.request, want) {
t.Fatalf("factory request = %#v, want %#v", factory.request, want)
}
if route.connectorKey != "documents" || route.connectorVersion != "1.2.3" || route.remoteMCP != client ||
!reflect.DeepEqual(client.calls, []string{"server/discover", "tools/list"}) ||
!reflect.DeepEqual(client.registered, []string{"search"}) {
t.Fatalf("remote client bootstrap = provenance:%s@%s route:%v calls:%#v registered:%#v",
route.connectorKey, route.connectorVersion, route.remoteMCP == client, client.calls, client.registered)
}
}
func TestBuildRemoteRouteReusesInProcessToolsListForSameAuthorizationIdentity(t *testing.T) {
client := &remoteMCPClientStub{}
host := &Host{
remoteMCPClientFactory: &remoteMCPFactoryRecorder{client: client},
remoteMCPTools: newRemoteMCPToolCache(),
}
request := testRemoteMCPRouteRequest("documents", 3, 12)
if _, err := host.buildRemoteRoute(context.Background(), request); err != nil {
t.Fatal(err)
}
request.Generation.Generation++
if _, err := host.buildRemoteRoute(context.Background(), request); err != nil {
t.Fatal(err)
}
if got := countRemoteMCPCalls(client.calls, "tools/list"); got != 1 {
t.Fatalf("tools/list calls = %d, want 1; calls=%#v", got, client.calls)
}
if got := countRemoteMCPCalls(client.calls, "server/discover"); got != 2 {
t.Fatalf("server/discover calls = %d, want 2; calls=%#v", got, client.calls)
}
}
func TestBuildRemoteRouteReloadsToolsListWhenAuthorizationIdentityChanges(t *testing.T) {
client := &remoteMCPClientStub{}
host := &Host{
remoteMCPClientFactory: &remoteMCPFactoryRecorder{client: client},
remoteMCPTools: newRemoteMCPToolCache(),
}
request := testRemoteMCPRouteRequest("documents", 3, 12)
if _, err := host.buildRemoteRoute(context.Background(), request); err != nil {
t.Fatal(err)
}
request.ConnectionVersion = 4
if _, err := host.buildRemoteRoute(context.Background(), request); err != nil {
t.Fatal(err)
}
if got := countRemoteMCPCalls(client.calls, "tools/list"); got != 2 {
t.Fatalf("tools/list calls = %d, want 2; calls=%#v", got, client.calls)
}
}
func TestBuildRemoteRouteDoesNotCacheAuthorizationRequiredToolsList(t *testing.T) {
client := &remoteMCPClientStub{listErr: &mcp.ModernHTTPError{
StatusCode: http.StatusPreconditionRequired,
Cause: &mcp.RPCError{Code: -33001, Message: "authorization required"},
}}
host := &Host{
remoteMCPClientFactory: &remoteMCPFactoryRecorder{client: client},
remoteMCPTools: newRemoteMCPToolCache(),
}
request := testRemoteMCPRouteRequest("gmail", 3, 12)
if _, err := host.buildRemoteRoute(context.Background(), request); err == nil {
t.Fatal("expected authorization-required tools/list to fail")
}
client.listErr = nil
if _, err := host.buildRemoteRoute(context.Background(), request); err != nil {
t.Fatal(err)
}
if got := countRemoteMCPCalls(client.calls, "tools/list"); got != 2 {
t.Fatalf("failed tools/list was cached: calls=%#v", client.calls)
}
}
func TestBuildRemoteRouteMapsAuthorizationRequired(t *testing.T) {
client := &remoteMCPClientStub{listErr: &mcp.ModernHTTPError{
StatusCode: http.StatusPreconditionRequired,
Cause: &mcp.RPCError{Code: -33001, Message: "authorization required"},
}}
host := &Host{remoteMCPClientFactory: &remoteMCPFactoryRecorder{client: client}}
implementation := market.RemoteStreamableHTTPImplementation{
ProtocolVersion: mcp.ModernProtocolVersion, BindingRef: "gmail.primary", ContractVersion: 1,
BindingContractHash: "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
}
_, err := host.buildRemoteRoute(context.Background(), market.RuntimeReconcileRequest{
OperationID: "operation-1", ConnectionID: "connection-1", Scope: market.OperationScope{AccountID: "account-1"},
Connector: market.Connector{Key: "gmail", Release: market.Release{
Version: "0.1.3", ReleaseDigest: "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd",
Manifest: market.Manifest{Implementation: market.Implementation{
Kind: market.ImplementationKindRemoteStreamableHTTP, RemoteStreamableHTTP: &implementation,
}},
}},
Generation: market.HostGeneration{BootEpoch: "boot-1", Generation: 1},
})
var domain *market.DomainError
if !errors.As(err, &domain) || domain.Code != market.ErrorCodeAuthorizationFailed || domain.Retryable {
t.Fatalf("buildRemoteRoute error = %#v", err)
}
if !client.closed {
t.Fatal("authorization-required discover/list left the remote MCP client open")
}
}
func testRemoteMCPRouteRequest(connectorKey string, connectionVersion, serverRevision uint64) market.RuntimeReconcileRequest {
implementation := market.RemoteStreamableHTTPImplementation{
ProtocolVersion: mcp.ModernProtocolVersion, BindingRef: connectorKey + ".primary", ContractVersion: 1,
BindingContractHash: "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
}
return market.RuntimeReconcileRequest{
OperationID: "operation-1", ConnectionID: "connection-1", Scope: market.OperationScope{AccountID: "account-1"},
Connector: market.Connector{Key: connectorKey, Release: market.Release{
Version: "1.2.3", ReleaseDigest: "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd",
Manifest: market.Manifest{Implementation: market.Implementation{
Kind: market.ImplementationKindRemoteStreamableHTTP, RemoteStreamableHTTP: &implementation,
}},
}},
Generation: market.HostGeneration{BootEpoch: "boot-1", Generation: 1},
ConnectionVersion: connectionVersion,
ServerRevision: serverRevision,
}
}
func countRemoteMCPCalls(calls []string, method string) int {
count := 0
for _, call := range calls {
if call == method {
count++
}
}
return count
}
@@ -0,0 +1,87 @@
package implementationhost
import (
"strings"
"sync"
market "github.com/tutti-os/tutti/packages/connector/daemon/core"
)
// remoteMCPToolCache keeps successful route-activation tools/list results in
// process memory. It is not Agent-visible truth and has no TTL; identity
// changes miss, and the Host drops the table on Close.
type remoteMCPToolCache struct {
mu sync.RWMutex
tools map[remoteMCPToolCacheKey][]mcpTool
}
type remoteMCPToolCacheKey struct {
accountID string
connectorKey string
releaseDigest string
connectionID string
connectionVersion uint64
serverRevision uint64
}
func newRemoteMCPToolCache() *remoteMCPToolCache {
return &remoteMCPToolCache{tools: make(map[remoteMCPToolCacheKey][]mcpTool)}
}
func remoteMCPToolCacheKeyFrom(request market.RuntimeReconcileRequest) remoteMCPToolCacheKey {
return remoteMCPToolCacheKey{
accountID: strings.TrimSpace(request.Scope.AccountID),
connectorKey: strings.TrimSpace(request.Connector.Key),
releaseDigest: strings.TrimSpace(request.Connector.Release.ReleaseDigest),
connectionID: strings.TrimSpace(request.ConnectionID),
connectionVersion: request.ConnectionVersion,
serverRevision: request.ServerRevision,
}
}
func (key remoteMCPToolCacheKey) complete() bool {
return key.accountID != "" && key.connectorKey != "" && key.releaseDigest != "" && key.connectionID != ""
}
func (cache *remoteMCPToolCache) lookup(key remoteMCPToolCacheKey) ([]mcpTool, bool) {
if cache == nil || !key.complete() {
return nil, false
}
cache.mu.RLock()
defer cache.mu.RUnlock()
tools, ok := cache.tools[key]
if !ok {
return nil, false
}
return cloneMCPTools(tools), true
}
func (cache *remoteMCPToolCache) store(key remoteMCPToolCacheKey, tools []mcpTool) {
if cache == nil || !key.complete() || len(tools) == 0 {
return
}
cache.mu.Lock()
defer cache.mu.Unlock()
cache.tools[key] = cloneMCPTools(tools)
}
func (cache *remoteMCPToolCache) clear() {
if cache == nil {
return
}
cache.mu.Lock()
defer cache.mu.Unlock()
cache.tools = make(map[remoteMCPToolCacheKey][]mcpTool)
}
func cloneMCPTools(tools []mcpTool) []mcpTool {
cloned := make([]mcpTool, len(tools))
for index, tool := range tools {
cloned[index] = mcpTool{
Name: tool.Name,
Description: tool.Description,
InputSchema: cloneJSONMap(tool.InputSchema),
}
}
return cloned
}
@@ -0,0 +1,101 @@
package implementationhost
import (
"context"
"io"
"sync"
"testing"
"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"
"github.com/tutti-os/tutti/packages/connector/runtime/mcp"
)
const routeObserverTestDigest = "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"
type routeObservationRecorder struct {
mu sync.Mutex
observations []RouteObservation
}
func (recorder *routeObservationRecorder) ObserveRoute(_ context.Context, observation RouteObservation) {
recorder.mu.Lock()
recorder.observations = append(recorder.observations, observation)
recorder.mu.Unlock()
}
type exitedMCPConnection struct{ delivered bool }
func (*exitedMCPConnection) Send([]byte) error { return nil }
func (*exitedMCPConnection) Close() error { return nil }
func (*exitedMCPConnection) CloseInput() error { return nil }
func (*exitedMCPConnection) Terminate() error { return nil }
func (*exitedMCPConnection) Kill() error { return nil }
func (connection *exitedMCPConnection) Recv() (agentruntime.ProcessFrame, error) {
if connection.delivered {
return agentruntime.ProcessFrame{}, io.EOF
}
connection.delivered = true
exitCode := 17
return agentruntime.ProcessFrame{ExitCode: &exitCode}, nil
}
func TestMonitorMCPRouteObservesOnlyUnexpectedCurrentRouteExit(t *testing.T) {
recorder := &routeObservationRecorder{}
routes := connectorruntime.NewRouteTable()
host := &Host{routes: routes, routeObserver: recorder}
generation := market.HostGeneration{BootEpoch: "boot-1", Generation: 4}
route := &connectorRoute{id: connectorRouteKey("connection-1", "calendar"), connectionID: "connection-1",
connectorKey: "calendar", releaseDigest: routeObserverTestDigest, generation: generation,
processes: connectorruntime.NewProcessGroup()}
if err := routes.Commit(route); err != nil {
t.Fatal(err)
}
client, err := mcp.NewStdioClient(mcp.StdioClientConfig{Connection: &exitedMCPConnection{}, ProcessName: "calendar"})
if err != nil {
t.Fatal(err)
}
host.monitorMCPRoute(route, client)
recorder.mu.Lock()
defer recorder.mu.Unlock()
if len(recorder.observations) != 1 {
t.Fatalf("route observations = %+v", recorder.observations)
}
observation := recorder.observations[0]
if observation.ConnectorKey != "calendar" || observation.ConnectionID != "connection-1" ||
observation.ReleaseDigest != routeObserverTestDigest || observation.Generation != generation ||
observation.ObservedAt.IsZero() {
t.Fatalf("route observation = %+v", observation)
}
if routes.IsCurrent(route) {
t.Fatal("exited MCP route remained current")
}
}
func TestMonitorMCPRouteDoesNotObserveIntentionalRemoval(t *testing.T) {
recorder := &routeObservationRecorder{}
routes := connectorruntime.NewRouteTable()
host := &Host{routes: routes, routeObserver: recorder}
generation := market.HostGeneration{BootEpoch: "boot-1", Generation: 1}
route := &connectorRoute{id: connectorRouteKey("connection-1", "calendar"), connectionID: "connection-1",
connectorKey: "calendar", releaseDigest: routeObserverTestDigest, generation: generation,
processes: connectorruntime.NewProcessGroup()}
if err := routes.Commit(route); err != nil {
t.Fatal(err)
}
if err := routes.Remove(route.id, generation, routeObserverTestDigest, time.Now().Add(time.Second)); err != nil {
t.Fatal(err)
}
client, err := mcp.NewStdioClient(mcp.StdioClientConfig{Connection: &exitedMCPConnection{}, ProcessName: "calendar"})
if err != nil {
t.Fatal(err)
}
host.monitorMCPRoute(route, client)
recorder.mu.Lock()
defer recorder.mu.Unlock()
if len(recorder.observations) != 0 {
t.Fatalf("intentional removal observations = %+v", recorder.observations)
}
}
@@ -0,0 +1,54 @@
package implementationhost
import market "github.com/tutti-os/tutti/packages/connector/daemon/core"
type SkillSummary = market.ConnectorSkillSummary
type ConnectorSummary = market.ConnectorSummary
type ConnectorInterfaceSummary = market.ConnectorInterfaceSummary
// ConnectorRoutingHint is a bounded, non-secret projection of one active
// route for Agent runtime preparation.
type ConnectorRoutingHint struct {
Key string
DisplayName string
Aliases []string
SkillRoot string
}
// ConnectorSummaries returns immutable discovery metadata already validated
// before each route was committed. It never rescans installed artifacts.
func (registry *RouteRegistry) ConnectorSummaries() []ConnectorSummary {
routes := registry.Routes()
connectors := make([]ConnectorSummary, 0, len(routes))
for _, route := range routes {
connectors = append(connectors, connectorSummaryFromDescriptor(route))
}
return connectors
}
func connectorSummaryFromDescriptor(route RouteDescriptor) ConnectorSummary {
interfaces := make([]ConnectorInterfaceSummary, 0, 2)
if route.HasMCP {
interfaces = append(interfaces, ConnectorInterfaceSummary{Kind: "mcp", ServerName: "connector",
ToolPrefix: route.ConnectorKey + "_", Status: string(route.InterfaceState("mcp"))})
}
if route.CLIInvocationCommand != "" {
interfaces = append(interfaces, ConnectorInterfaceSummary{Kind: "cli", Command: route.CLIInvocationCommand,
Status: string(route.InterfaceState("cli"))})
}
return ConnectorSummary{Key: route.ConnectorKey, Version: route.ConnectorVersion, Name: route.DisplayName, Description: route.Description,
Skills: append([]SkillSummary(nil), route.Skills...), Interfaces: interfaces}
}
// RoutingHints returns a detached snapshot for Agent runtime preparation.
func (registry *RouteRegistry) RoutingHints() []ConnectorRoutingHint {
routes := registry.Routes()
hints := make([]ConnectorRoutingHint, 0, len(routes))
for _, route := range routes {
hints = append(hints, ConnectorRoutingHint{
Key: route.ConnectorKey, DisplayName: route.DisplayName,
Aliases: append([]string(nil), route.RoutingAliases...), SkillRoot: route.SkillRoot,
})
}
return hints
}
@@ -0,0 +1,84 @@
package implementationhost
import (
"slices"
"testing"
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"
)
func TestRouteRegistryProjectsDetachedConnectorMetadata(t *testing.T) {
route := &connectorRoute{
id: "account-1\x00calendar", connectionID: "account-1", connectorVersion: "1.2.3", releaseDigest: "digest",
generation: market.HostGeneration{BootEpoch: "boot-1", Generation: 1},
connectorKey: "calendar", displayName: "Calendar", description: "Manage meetings",
routingAliases: []string{"日历"}, skillRoot: "/verified/skills",
skills: []connectorartifact.SkillSummary{{Name: "standup", Title: "Standup", Description: "Prepare a standup"}},
mcpTools: map[string]registeredMCPTool{"calendar_list": {}}, cliCommand: "tutti-connector-calendar", cliInvocationCommand: "calendar-cli",
cliContractHash: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
cliCommands: []market.CLICommand{{Name: "events", Arguments: []string{"events"},
InputSchema: map[string]any{"type": "object"}, TimeoutMS: 10_000}},
processes: connectorruntime.NewProcessGroup(),
}
table := connectorruntime.NewRouteTable()
if err := table.Commit(route); err != nil {
t.Fatal(err)
}
registry := NewRouteRegistry()
registry.attach(table)
summaries := registry.ConnectorSummaries()
if len(summaries) != 1 || summaries[0].Key != "calendar" || summaries[0].Version != "1.2.3" || len(summaries[0].Skills) != 1 ||
len(summaries[0].Interfaces) != 2 || summaries[0].Interfaces[0].Kind != "mcp" || summaries[0].Interfaces[1].Kind != "cli" {
t.Fatalf("summaries = %#v", summaries)
}
if summaries[0].Interfaces[1].Command != "calendar-cli" {
t.Fatalf("CLI command = %q", summaries[0].Interfaces[1].Command)
}
hints := registry.RoutingHints()
if len(hints) != 1 || hints[0].SkillRoot != "/verified/skills" || !slices.Equal(hints[0].Aliases, []string{"日历"}) {
t.Fatalf("hints = %#v", hints)
}
routes := registry.Routes()
if len(routes) != 1 || routes[0].ConnectionID != "account-1" || routes[0].ConnectorVersion != "1.2.3" ||
routes[0].ReleaseDigest != "digest" || routes[0].CLIContractHash == "" || len(routes[0].CLICommands) != 1 {
t.Fatalf("routes = %#v", routes)
}
summaries[0].Skills[0].Name = "mutated"
hints[0].Aliases[0] = "mutated"
routes[0].CLICommands[0].Arguments[0] = "mutated"
routes[0].CLICommands[0].InputSchema["type"] = "mutated"
refetchedRoute := registry.Routes()[0]
if registry.ConnectorSummaries()[0].Skills[0].Name != "standup" || registry.RoutingHints()[0].Aliases[0] != "日历" ||
refetchedRoute.CLICommands[0].Arguments[0] != "events" || refetchedRoute.CLICommands[0].InputSchema["type"] != "object" {
t.Fatal("route projection leaked mutable state")
}
}
func TestCommittedRouteSummaryDoesNotDependOnCapabilityPublication(t *testing.T) {
route := &connectorRoute{
id: "account-1\x00calendar", releaseDigest: "digest",
generation: market.HostGeneration{BootEpoch: "boot-1", Generation: 1},
connectorKey: "calendar", displayName: "Calendar", description: "Manage meetings",
skills: []connectorartifact.SkillSummary{{Name: "standup", Title: "Standup", Description: "Prepare a standup"}},
mcpTools: map[string]registeredMCPTool{"calendar_list": {}}, processes: connectorruntime.NewProcessGroup(),
readiness: market.RuntimeReadiness{State: market.RuntimeReadinessReady,
Interfaces: []market.InterfaceReadiness{{Kind: "mcp", State: market.RuntimeReadinessReady}}},
}
table := connectorruntime.NewRouteTable()
if err := table.Commit(route); err != nil {
t.Fatal(err)
}
table.SetPublished(false)
registry := NewRouteRegistry()
registry.attach(table)
if summaries := registry.ConnectorSummaries(); len(summaries) != 0 {
t.Fatalf("published summaries = %#v", summaries)
}
summary := connectorSummaryFromDescriptor(routeDescriptor(route))
if summary.Key != "calendar" || len(summary.Skills) != 1 || len(summary.Interfaces) != 1 {
t.Fatalf("committed summary = %#v", summary)
}
}
@@ -0,0 +1,121 @@
package implementationhost
import (
"sort"
"sync"
"sync/atomic"
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"
)
type RouteRegistry struct {
mu sync.RWMutex
routes *connectorruntime.RouteTable
revision atomic.Uint64
}
type RouteDescriptor struct {
ConnectionID string
ConnectorKey string
ConnectorVersion string
ReleaseDigest string
Generation market.HostGeneration
DisplayName string
Description string
RoutingAliases []string
SkillRoot string
Skills []connectorartifact.SkillSummary
HasMCP bool
CLICommand string
CLIInvocationCommand string
CLIContractHash string
CLICommands []market.CLICommand
Readiness market.RuntimeReadiness
}
func (descriptor RouteDescriptor) InterfaceState(kind string) market.RuntimeReadinessState {
for _, readiness := range descriptor.Readiness.Interfaces {
if readiness.Kind == kind {
return readiness.State
}
}
return market.RuntimeReadinessFailed
}
func NewRouteRegistry() *RouteRegistry { return &RouteRegistry{} }
func (registry *RouteRegistry) Revision() uint64 {
if registry == nil {
return 0
}
return registry.revision.Load()
}
func (registry *RouteRegistry) notifyChanged() {
if registry != nil {
registry.revision.Add(1)
}
}
func (registry *RouteRegistry) attach(routes *connectorruntime.RouteTable) {
registry.mu.Lock()
registry.routes = routes
registry.mu.Unlock()
}
func (registry *RouteRegistry) activeRoutes() []*connectorRoute {
registry.mu.RLock()
table := registry.routes
registry.mu.RUnlock()
if table == nil {
return nil
}
portable := table.PublishedRoutes()
routes := make([]*connectorRoute, 0, len(portable))
for _, candidate := range portable {
if route, ok := candidate.(*connectorRoute); ok {
routes = append(routes, route)
}
}
return routes
}
func (registry *RouteRegistry) Routes() []RouteDescriptor {
routes := registry.activeRoutes()
result := make([]RouteDescriptor, 0, len(routes))
for _, route := range routes {
result = append(result, routeDescriptor(route))
}
sort.Slice(result, func(left, right int) bool {
if result[left].ConnectorKey != result[right].ConnectorKey {
return result[left].ConnectorKey < result[right].ConnectorKey
}
return result[left].ConnectionID < result[right].ConnectionID
})
return result
}
func routeDescriptor(route *connectorRoute) RouteDescriptor {
return RouteDescriptor{ConnectionID: route.connectionID, ConnectorKey: route.connectorKey,
ConnectorVersion: route.connectorVersion, ReleaseDigest: route.releaseDigest, Generation: route.generation,
DisplayName: route.displayName,
Description: route.description, RoutingAliases: append([]string(nil), route.routingAliases...),
SkillRoot: route.skillRoot, Skills: append([]connectorartifact.SkillSummary(nil), route.skills...),
HasMCP: len(route.mcpTools) > 0, CLICommand: route.cliCommand, CLIInvocationCommand: route.cliInvocationCommand,
CLIContractHash: route.cliContractHash,
CLICommands: cloneCLICommands(route.cliCommands),
Readiness: cloneRuntimeReadiness(route.readiness)}
}
func cloneCLICommands(commands []market.CLICommand) []market.CLICommand {
result := make([]market.CLICommand, 0, len(commands))
for _, command := range commands {
cloned := command
cloned.Arguments = append([]string(nil), command.Arguments...)
cloned.InputSchema = cloneJSONMap(command.InputSchema)
result = append(result, cloned)
}
return result
}
@@ -0,0 +1,82 @@
package implementationhost
import (
"errors"
"fmt"
"os"
"path/filepath"
"runtime"
"strings"
market "github.com/tutti-os/tutti/packages/connector/daemon/core"
connectorruntime "github.com/tutti-os/tutti/packages/connector/runtime"
)
func (host *Host) deactivateConnector(request market.RuntimeDeactivationRequest) error {
match := func(route connectorruntime.ManagedRoute) bool {
candidate, ok := route.(*connectorRoute)
return ok && candidate.connectorKey == request.ConnectorKey
}
routeErr := host.routes.RemoveMatching(match, request.Generation, request.Deadline)
host.authorizationMu.Lock()
authorizationRoutes := make([]*connectorRoute, 0)
for key, route := range host.authorizationRoutes {
if route.connectorKey != request.ConnectorKey {
continue
}
delete(host.authorizationRoutes, key)
authorizationRoutes = append(authorizationRoutes, route)
}
host.authorizationMu.Unlock()
var authorizationErrs []error
for _, route := range authorizationRoutes {
host.authorizationProvider.cancelAuthorizationSessionByRoute(route.id)
route.Fence()
if err := route.Close(request.Deadline); err != nil {
// Keep a fenced route reachable so an idempotent uninstall retry can
// finish snapshot/process cleanup after a transient close failure.
host.authorizationMu.Lock()
if host.authorizationRoutes[route.id] == nil {
host.authorizationRoutes[route.id] = route
}
host.authorizationMu.Unlock()
authorizationErrs = append(authorizationErrs, err)
}
}
shimErr := host.removeOrphanedConnectorCLIShim(request.ConnectorKey)
host.notifyRouteChanged()
return errors.Join(routeErr, errors.Join(authorizationErrs...), shimErr)
}
func (host *Host) removeOrphanedConnectorCLIShim(connectorKey string) error {
if !hostIdentityPattern.MatchString(connectorKey) {
return errors.New("connector CLI shim removal identity is invalid")
}
if !filepath.IsAbs(host.binDir) {
return nil
}
command := "tutti-connector-" + connectorKey
if runtime.GOOS == "windows" {
command += ".cmd"
}
path := filepath.Join(host.binDir, command)
content, err := os.ReadFile(path)
if errors.Is(err, os.ErrNotExist) {
return nil
}
if err != nil {
return fmt.Errorf("read connector CLI shim before removal: %w", err)
}
managedMarker := "export TUTTI_CONNECTOR_KEY=" + shellQuote(connectorKey) + "\n"
if runtime.GOOS == "windows" {
managedMarker = "set \"TUTTI_CONNECTOR_KEY=" + connectorKey + "\"\r\n"
}
if !strings.Contains(string(content), managedMarker) {
return errors.New("connector CLI shim removal target is not Tutti-managed")
}
if err := os.Remove(path); err != nil && !errors.Is(err, os.ErrNotExist) {
return fmt.Errorf("remove connector CLI shim: %w", err)
}
return nil
}
@@ -0,0 +1,213 @@
package implementationhost
import (
"context"
"os"
"path/filepath"
"runtime"
"testing"
"time"
market "github.com/tutti-os/tutti/packages/connector/daemon/core"
connectorruntime "github.com/tutti-os/tutti/packages/connector/runtime"
)
func TestDeactivateRuntimeAllConnectionsRemovesRotatedConnectorRoutes(t *testing.T) {
generation := market.HostGeneration{BootEpoch: "boot-1", Generation: 7}
host := &Host{
routes: connectorruntime.NewRouteTable(),
authorizationRoutes: make(map[string]*connectorRoute),
authorizationProvider: newManagedCredentialAuthorizationProvider(nil),
registry: NewRouteRegistry(),
mcpRegistry: NewMCPRegistry(),
}
host.authorizationProvider.host = host
shimPath := filepath.Join(t.TempDir(), "tutti-connector-github")
routes := []*connectorRoute{
testUninstallRoute("connection-before-rotation", "github", "superseded-release", generation),
testUninstallRoute("connection-after-rotation", "github", "release-1", generation),
}
routes[0].cliShimPath = shimPath
routes[0].cliShimContent = []byte("shim")
if err := os.WriteFile(shimPath, routes[0].cliShimContent, 0o700); err != nil {
t.Fatal(err)
}
for _, route := range routes {
if err := host.routes.Commit(route); err != nil {
t.Fatal(err)
}
}
unrelated := testUninstallRoute("connection-other", "notion", "release-2", generation)
if err := host.routes.Commit(unrelated); err != nil {
t.Fatal(err)
}
if err := host.DeactivateRuntime(context.Background(), market.RuntimeDeactivationRequest{
ConnectionID: "connection-after-rotation", ConnectorKey: "github", ReleaseDigest: "release-1",
AllConnections: true, Generation: generation, Deadline: time.Now().Add(time.Second),
}); err != nil {
t.Fatal(err)
}
for _, route := range routes {
if host.routes.Route(route.id) != nil || !route.processes.IsFenced() {
t.Fatalf("route %q survived all-connection deactivation", route.id)
}
}
if host.routes.Route(unrelated.id) != unrelated {
t.Fatal("unrelated connector route was removed")
}
if _, err := os.Stat(shimPath); !os.IsNotExist(err) {
t.Fatalf("CLI shim still exists: %v", err)
}
}
func TestDisabledReconcileRemovesRotatedConnectorRoutes(t *testing.T) {
generation := market.HostGeneration{BootEpoch: "boot-1", Generation: 7}
host := &Host{
routes: connectorruntime.NewRouteTable(),
authorizationRoutes: make(map[string]*connectorRoute),
authorizationProvider: newManagedCredentialAuthorizationProvider(nil),
registry: NewRouteRegistry(),
mcpRegistry: NewMCPRegistry(),
}
host.authorizationProvider.host = host
routes := []*connectorRoute{
testUninstallRoute("connection-before-rotation", "github", "superseded-release", generation),
testUninstallRoute("connection-after-rotation", "github", "release-1", generation),
}
for _, route := range routes {
if err := host.routes.Commit(route); err != nil {
t.Fatal(err)
}
}
receipt, err := host.Reconcile(context.Background(), ReconcileRequest{Runtime: market.RuntimeReconcileRequest{
OperationID: "disable-github", ConnectionID: "connection-after-rotation",
Connector: market.Connector{Key: "github", Installation: market.Installation{
InstalledReleaseDigest: "release-1",
}},
Enabled: false, Generation: generation,
}})
if err != nil {
t.Fatal(err)
}
for _, route := range routes {
if host.routes.Route(route.id) != nil || !route.processes.IsFenced() {
t.Fatalf("route %q survived disabled convergence", route.id)
}
}
if receipt.Readiness.State != market.RuntimeReadinessBlocked ||
receipt.Readiness.ReasonCode != market.RuntimeReadinessReasonRuntimeDisabled {
t.Fatalf("disabled receipt = %#v", receipt)
}
}
func TestDisabledCandidateReconcileReturnsRequestedReleaseDigest(t *testing.T) {
host := &Host{
routes: connectorruntime.NewRouteTable(),
authorizationRoutes: make(map[string]*connectorRoute),
authorizationProvider: newManagedCredentialAuthorizationProvider(nil),
registry: NewRouteRegistry(),
mcpRegistry: NewMCPRegistry(),
}
host.authorizationProvider.host = host
const releaseDigest = "candidate-release"
receipt, err := host.Reconcile(context.Background(), ReconcileRequest{Runtime: market.RuntimeReconcileRequest{
OperationID: "disable-unauthed-linear", ConnectionID: "account-linear",
Connector: market.Connector{
Key: "linear",
Release: market.Release{ReleaseDigest: releaseDigest},
Installation: market.Installation{
State: market.InstallationStateInstalling,
CandidateReleaseDigest: releaseDigest,
},
},
Enabled: false, Generation: market.HostGeneration{BootEpoch: "boot-1", Generation: 8},
}})
if err != nil {
t.Fatal(err)
}
if receipt.ReleaseDigest != releaseDigest {
t.Fatalf("receipt release digest = %q, want %q", receipt.ReleaseDigest, releaseDigest)
}
if receipt.Readiness.State != market.RuntimeReadinessBlocked ||
receipt.Readiness.ReasonCode != market.RuntimeReadinessReasonRuntimeDisabled {
t.Fatalf("disabled candidate receipt = %#v", receipt)
}
}
func TestDeactivateRuntimeAllConnectionsCancelsPendingAuthorizationRoute(t *testing.T) {
host := &Host{
routes: connectorruntime.NewRouteTable(),
authorizationRoutes: make(map[string]*connectorRoute),
authorizationProvider: newManagedCredentialAuthorizationProvider(nil),
registry: NewRouteRegistry(),
mcpRegistry: NewMCPRegistry(),
}
host.authorizationProvider.host = host
generation := market.HostGeneration{BootEpoch: "boot-1", Generation: 3}
route := testUninstallRoute("account-pending", "github", "superseded-release", market.HostGeneration{BootEpoch: "authorization", Generation: 2})
host.authorizationRoutes[route.id] = route
canceled := make(chan struct{})
const operationID = "authorization-pending"
host.authorizationProvider.sessions[operationID] = &credentialBrokerSession{
operationID: operationID, route: route,
cancel: func() { close(canceled) }, changed: make(chan struct{}), state: market.AuthorizationStatePending,
}
host.authorizationProvider.activeByRoute[route.id] = operationID
if err := host.DeactivateRuntime(context.Background(), market.RuntimeDeactivationRequest{
ConnectionID: "current-connection", ConnectorKey: "github", ReleaseDigest: "release-1",
AllConnections: true, Generation: generation, Deadline: time.Now().Add(time.Second),
}); err != nil {
t.Fatal(err)
}
select {
case <-canceled:
default:
t.Fatal("pending credential broker session was not canceled")
}
if host.authorizationRoutes[route.id] != nil || host.authorizationProvider.sessions[operationID] != nil || !route.processes.IsFenced() {
t.Fatal("pending authorization route survived uninstall")
}
}
func TestDeactivateRuntimeAllConnectionsRemovesOrphanedManagedCLIShim(t *testing.T) {
binDir := t.TempDir()
host := &Host{
binDir: binDir,
routes: connectorruntime.NewRouteTable(),
authorizationRoutes: make(map[string]*connectorRoute),
authorizationProvider: newManagedCredentialAuthorizationProvider(nil),
registry: NewRouteRegistry(),
mcpRegistry: NewMCPRegistry(),
}
host.authorizationProvider.host = host
command := "tutti-connector-github"
content := "#!/bin/sh\nexport TUTTI_CONNECTOR_KEY='github'\n"
if runtime.GOOS == "windows" {
command += ".cmd"
content = "@echo off\r\nset \"TUTTI_CONNECTOR_KEY=github\"\r\n"
}
shimPath := filepath.Join(binDir, command)
if err := os.WriteFile(shimPath, []byte(content), 0o700); err != nil {
t.Fatal(err)
}
if err := host.DeactivateRuntime(context.Background(), market.RuntimeDeactivationRequest{
ConnectionID: "missing", ConnectorKey: "github", ReleaseDigest: "release-1", AllConnections: true,
Generation: market.HostGeneration{BootEpoch: "boot-1", Generation: 1}, Deadline: time.Now().Add(time.Second),
}); err != nil {
t.Fatal(err)
}
if _, err := os.Stat(shimPath); !os.IsNotExist(err) {
t.Fatalf("orphaned CLI shim still exists: %v", err)
}
}
func testUninstallRoute(connectionID, connectorKey, releaseDigest string, generation market.HostGeneration) *connectorRoute {
return &connectorRoute{
id: connectorRouteKey(connectionID, connectorKey), connectionID: connectionID,
connectorKey: connectorKey, releaseDigest: releaseDigest, generation: generation,
mcpTools: make(map[string]registeredMCPTool), processes: connectorruntime.NewProcessGroup(),
}
}