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
331 lines
14 KiB
Go
331 lines
14 KiB
Go
package agentruntime
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/tutti-os/tutti/packages/agent/daemon/providerregistry"
|
|
)
|
|
|
|
func newMigratedProviderAdapters(
|
|
transport ProcessTransport,
|
|
host HostMetadata,
|
|
commandResolver ProviderCommandResolver,
|
|
commandNetworkAccessPolicy CommandNetworkAccessPolicy,
|
|
) []Adapter {
|
|
descriptors := providerregistry.Migrated()
|
|
adapters := make([]Adapter, 0, len(descriptors))
|
|
for _, descriptor := range descriptors {
|
|
if err := providerregistry.Validate(descriptor); err != nil {
|
|
panic(fmt.Sprintf("invalid migrated provider descriptor: %v", err))
|
|
}
|
|
options := providerAdapterOptions{}
|
|
if descriptor.Runtime.Kind == providerregistry.RuntimeKindCodexAppServer &&
|
|
commandNetworkAccessPolicy != nil {
|
|
options.commandNetworkAccess = commandNetworkAccessPolicy(descriptor.Identity.ID)
|
|
}
|
|
adapter := newAdapterFromProviderDescriptor(descriptor, transport, host, commandResolver, options)
|
|
if adapter == nil {
|
|
panic(fmt.Sprintf("provider %q has unsupported runtime kind %q", descriptor.Identity.ID, descriptor.Runtime.Kind))
|
|
}
|
|
if provider := strings.TrimSpace(adapter.Provider()); provider != descriptor.Identity.ID {
|
|
panic(fmt.Sprintf("provider descriptor %q constructed adapter for %q", descriptor.Identity.ID, provider))
|
|
}
|
|
adapters = append(adapters, adapter)
|
|
}
|
|
return adapters
|
|
}
|
|
|
|
type providerAdapterOptions struct {
|
|
commandNetworkAccess bool
|
|
}
|
|
|
|
func newAdapterFromProviderDescriptor(
|
|
descriptor providerregistry.ProviderDescriptor,
|
|
transport ProcessTransport,
|
|
host HostMetadata,
|
|
commandResolver ProviderCommandResolver,
|
|
options providerAdapterOptions,
|
|
) Adapter {
|
|
switch descriptor.Runtime.Kind {
|
|
case providerregistry.RuntimeKindCodexAppServer:
|
|
return newAppServerAdapter(
|
|
transport,
|
|
host,
|
|
appServerAdapterConfig{
|
|
provider: descriptor.Identity.ID,
|
|
runtimeName: descriptor.Runtime.Name,
|
|
displayName: descriptor.Identity.DisplayName,
|
|
command: append([]string(nil), descriptor.Runtime.Command...),
|
|
clientInfoName: descriptor.Runtime.ClientInfoName,
|
|
authRequiredMessage: descriptor.Runtime.AuthRequiredMessage,
|
|
skillRootsStrategy: descriptor.Runtime.AppServerSkillRoots,
|
|
rateLimits: providerDescriptorHasCapability(descriptor, CapabilityRateLimits),
|
|
nativeSessionFork: descriptor.Runtime.NativeSessionFork,
|
|
sessionForkUserAgentBrand: descriptor.Runtime.AppServerFork.UserAgentBrand,
|
|
sessionForkThroughTurnMinVersion: descriptor.Runtime.AppServerFork.ThroughTurnMinVersion,
|
|
commandNetworkAccess: options.commandNetworkAccess,
|
|
},
|
|
commandResolver,
|
|
)
|
|
case providerregistry.RuntimeKindStandardACP:
|
|
switch descriptor.Runtime.StandardACP.AdapterStrategy {
|
|
case "", providerregistry.StandardACPAdapterStrategyGeneric:
|
|
return newStandardACPAdapterFromProviderDescriptor(descriptor, transport, host, commandResolver)
|
|
case providerregistry.StandardACPAdapterStrategyCursor:
|
|
return newCursorAdapterFromProviderDescriptor(descriptor, transport, host, cursorACPCommandResolver)
|
|
case providerregistry.StandardACPAdapterStrategyNexight:
|
|
return newNexightAdapterFromProviderDescriptor(descriptor, transport, host, commandResolver)
|
|
case providerregistry.StandardACPAdapterStrategyOpenClaw:
|
|
return newOpenClawAdapterFromProviderDescriptor(descriptor, transport, host, commandResolver)
|
|
case providerregistry.StandardACPAdapterStrategyOpenCode:
|
|
return newOpenCodeAdapterFromProviderDescriptor(descriptor, transport, host, commandResolver)
|
|
default:
|
|
return nil
|
|
}
|
|
case providerregistry.RuntimeKindClaudeSDK:
|
|
return NewClaudeCodeSDKAdapter(transport)
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func providerDescriptorHasCapability(descriptor providerregistry.ProviderDescriptor, capability string) bool {
|
|
for _, candidate := range descriptor.ComposerProfile.Capabilities {
|
|
if candidate == capability {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func newStandardACPAdapterFromProviderDescriptor(
|
|
descriptor providerregistry.ProviderDescriptor,
|
|
transport ProcessTransport,
|
|
host HostMetadata,
|
|
commandResolver ProviderCommandResolver,
|
|
) *standardACPAdapter {
|
|
modeIDs := make(map[string]string, len(descriptor.Runtime.StandardACP.PermissionModes))
|
|
for _, mode := range descriptor.Runtime.StandardACP.PermissionModes {
|
|
modeIDs[strings.TrimSpace(mode.InputID)] = strings.TrimSpace(mode.RuntimeID)
|
|
}
|
|
settingsEnvironment := descriptor.Runtime.StandardACP.SettingsEnvironment
|
|
standardACP := descriptor.Runtime.StandardACP
|
|
defaultRuntimeModeID := strings.TrimSpace(descriptor.Runtime.StandardACP.DefaultPermissionModeRuntimeID)
|
|
return &standardACPAdapter{
|
|
config: standardACPConfig{
|
|
provider: descriptor.Identity.ID,
|
|
adapterName: descriptor.Runtime.Name,
|
|
command: append([]string(nil), descriptor.Runtime.Command...),
|
|
defaultTitle: descriptor.Identity.DisplayName,
|
|
defaultTitleAliases: append([]string{descriptor.Identity.DisplayName, descriptor.Identity.ID}, descriptor.Identity.Aliases...),
|
|
authRequiredMessage: descriptor.Runtime.AuthRequiredMessage,
|
|
permissionModeID: func(mode string) string {
|
|
if runtimeModeID := modeIDs[strings.TrimSpace(mode)]; runtimeModeID != "" {
|
|
return runtimeModeID
|
|
}
|
|
return defaultRuntimeModeID
|
|
},
|
|
initializeParams: func() map[string]any { return defaultACPInitializeParams(host) },
|
|
env: func(session Session) []string {
|
|
env := standardACPEnv(session, host)
|
|
if value := runtimeSettingsEnvironmentValue(settingsEnvironment, session); value != "" {
|
|
env = append(env, settingsEnvironment.Variable+"="+value)
|
|
}
|
|
return env
|
|
},
|
|
commandResolver: commandResolver,
|
|
planModeRuntimeID: strings.TrimSpace(standardACP.PlanModeRuntimeID),
|
|
planModeDisabledRuntimeID: strings.TrimSpace(
|
|
standardACP.PlanModeDisabledRuntimeID,
|
|
),
|
|
projectCurrentMode: standardACP.ProjectCurrentMode,
|
|
startupDiagnostics: standardACP.StartupDiagnostics,
|
|
},
|
|
transport: transport,
|
|
host: host,
|
|
sessions: make(map[string]*standardACPSession),
|
|
inputUnits: providerInputUnitTrackerForTransport(transport),
|
|
}
|
|
}
|
|
|
|
type StandardACPAdapterConfig struct {
|
|
Provider string
|
|
Name string
|
|
DisplayName string
|
|
Command []string
|
|
AuthMessage string
|
|
ToolAliases map[string]string
|
|
ModelConfigOptionID string
|
|
ModelDescriptionFormat string
|
|
PermissionConfigOptionID string
|
|
ReasoningConfigOptionID string
|
|
RestrictConfigOptions bool
|
|
PermissionModes map[string]string
|
|
AutomaticPermissionDecisions map[string]string
|
|
PlanModeRuntimeID string
|
|
PlanModeDisabledRuntimeID string
|
|
PlanModeUsesLaunchPermission bool
|
|
LaunchPermission *StandardACPLaunchPermissionSetting
|
|
SetModelReasoningEffortMeta bool
|
|
Capabilities []string
|
|
AgentTargetID string
|
|
InstallationID string
|
|
ExecutableIdentity *ExecutableIdentity
|
|
Env []string
|
|
// StartupTimeout bounds initialize/session-new calls for callers that own a
|
|
// provider-specific cold-start policy. Zero keeps the normal ACP timeout.
|
|
StartupTimeout time.Duration
|
|
}
|
|
|
|
// NewStandardACPAdapter creates the generic, data-driven ACP adapter used by
|
|
// verified Agent Extension installations. It intentionally exposes no hooks
|
|
// for loading extension code into the daemon.
|
|
func NewStandardACPAdapter(config StandardACPAdapterConfig, transport ProcessTransport, host HostMetadata) (Adapter, error) {
|
|
provider := strings.TrimSpace(config.Provider)
|
|
if provider == "" || len(config.Command) == 0 || strings.TrimSpace(config.Command[0]) == "" {
|
|
return nil, fmt.Errorf("standard ACP provider and command are required")
|
|
}
|
|
modelDescriptionFormat := strings.TrimSpace(config.ModelDescriptionFormat)
|
|
if modelDescriptionFormat != "" && modelDescriptionFormat != StandardACPModelDescriptionMetadataFormatCreditConsumptionMultiplierV1 {
|
|
return nil, errors.New("standard ACP model description metadata format is unsupported")
|
|
}
|
|
launchPermission, err := validateStandardACPLaunchPermissionSetting(config.Command, config.LaunchPermission)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if config.PlanModeUsesLaunchPermission {
|
|
planRuntimeID := strings.TrimSpace(config.PlanModeRuntimeID)
|
|
if launchPermission == nil || !standardACPLaunchSettingValuePattern.MatchString(planRuntimeID) {
|
|
return nil, errors.New("standard ACP launch-permission Plan mode declaration is invalid")
|
|
}
|
|
for _, permissionRuntimeID := range launchPermission.Values {
|
|
if planRuntimeID == permissionRuntimeID {
|
|
return nil, errors.New("standard ACP launch-permission Plan mode must use a distinct runtime value")
|
|
}
|
|
}
|
|
}
|
|
host = normalizeHostMetadata(host)
|
|
startupTimeout := config.StartupTimeout
|
|
if startupTimeout <= 0 {
|
|
startupTimeout = acpStartCallTimeout
|
|
}
|
|
permissionModes := cloneStandardACPToolAliases(config.PermissionModes)
|
|
adapter := &standardACPAdapter{
|
|
config: standardACPConfig{
|
|
provider: provider,
|
|
deferApprovalUntilToolInput: standardACPProviderBehaviorFor(provider).deferApprovalUntilToolInput,
|
|
adapterName: strings.TrimSpace(config.Name),
|
|
command: append([]string(nil), config.Command...),
|
|
defaultTitle: strings.TrimSpace(config.DisplayName),
|
|
defaultTitleAliases: []string{strings.TrimSpace(config.DisplayName), provider},
|
|
authRequiredMessage: strings.TrimSpace(config.AuthMessage),
|
|
toolAliases: cloneStandardACPToolAliases(config.ToolAliases),
|
|
modelConfigOptionID: strings.TrimSpace(config.ModelConfigOptionID),
|
|
modelDescriptionFormat: modelDescriptionFormat,
|
|
permissionConfigOptionID: strings.TrimSpace(config.PermissionConfigOptionID),
|
|
reasoningConfigOptionID: strings.TrimSpace(config.ReasoningConfigOptionID),
|
|
restrictConfigOptions: config.RestrictConfigOptions,
|
|
capabilities: append([]string(nil), config.Capabilities...),
|
|
agentTargetID: strings.TrimSpace(config.AgentTargetID),
|
|
installationID: strings.TrimSpace(config.InstallationID),
|
|
executableIdentity: cloneExecutableIdentity(config.ExecutableIdentity),
|
|
startupTimeout: startupTimeout,
|
|
permissionModeID: func(input string) string { return permissionModes[strings.ToLower(strings.TrimSpace(input))] },
|
|
planModeRuntimeID: strings.TrimSpace(config.PlanModeRuntimeID),
|
|
planModeDisabledRuntimeID: strings.TrimSpace(config.PlanModeDisabledRuntimeID),
|
|
planModeUsesLaunchPermission: config.PlanModeUsesLaunchPermission,
|
|
failOnSetModeError: strings.TrimSpace(config.PlanModeRuntimeID) != "" && !config.PlanModeUsesLaunchPermission,
|
|
launchPermission: launchPermission,
|
|
setModelReasoningEffortMeta: config.SetModelReasoningEffortMeta,
|
|
initializeParams: func() map[string]any { return defaultACPInitializeParams(host) },
|
|
env: func(session Session) []string {
|
|
return append(standardACPEnv(session, host), config.Env...)
|
|
},
|
|
},
|
|
transport: transport, host: host, sessions: make(map[string]*standardACPSession),
|
|
inputUnits: providerInputUnitTrackerForTransport(transport),
|
|
}
|
|
if decisions := automaticPermissionDecisionFromMap(config.AutomaticPermissionDecisions); decisions != nil {
|
|
adapter.config.automaticPermissionDecision = decisions
|
|
}
|
|
return adapter, nil
|
|
}
|
|
|
|
type standardACPProviderBehavior struct {
|
|
deferApprovalUntilToolInput bool
|
|
}
|
|
|
|
// standardACPProviderBehaviorFor owns trusted protocol differences for
|
|
// externalized Agent Extensions. Extension manifests cannot opt into these
|
|
// behaviors themselves.
|
|
func standardACPProviderBehaviorFor(provider string) standardACPProviderBehavior {
|
|
switch strings.TrimSpace(provider) {
|
|
case "acp:kimi-code":
|
|
return standardACPProviderBehavior{deferApprovalUntilToolInput: true}
|
|
default:
|
|
return standardACPProviderBehavior{}
|
|
}
|
|
}
|
|
|
|
func automaticPermissionDecisionFromMap(input map[string]string) func(string) string {
|
|
decisions := make(map[string]string, len(input))
|
|
for id, decision := range input {
|
|
normalizedID := strings.ToLower(strings.TrimSpace(id))
|
|
normalizedDecision := strings.TrimSpace(decision)
|
|
if normalizedID != "" && (normalizedDecision == "approved" || normalizedDecision == "denied") {
|
|
decisions[normalizedID] = normalizedDecision
|
|
}
|
|
}
|
|
if len(decisions) == 0 {
|
|
return nil
|
|
}
|
|
return func(permissionModeID string) string {
|
|
return decisions[strings.ToLower(strings.TrimSpace(permissionModeID))]
|
|
}
|
|
}
|
|
|
|
func cloneStandardACPToolAliases(input map[string]string) map[string]string {
|
|
result := make(map[string]string, len(input))
|
|
for key, value := range input {
|
|
result[strings.ToLower(strings.TrimSpace(key))] = strings.TrimSpace(value)
|
|
}
|
|
return result
|
|
}
|
|
|
|
func runtimeSettingsEnvironmentValue(
|
|
descriptor providerregistry.RuntimeSettingsEnvironmentDescriptor,
|
|
session Session,
|
|
) string {
|
|
settings := session.SettingsValue()
|
|
values := make(map[string]string, len(descriptor.JSONFields))
|
|
for _, field := range descriptor.JSONFields {
|
|
var value string
|
|
if field.Setting == providerregistry.RuntimeSettingFieldModel {
|
|
value = strings.TrimSpace(settings.Model)
|
|
}
|
|
if value != "" {
|
|
values[field.JSONKey] = value
|
|
}
|
|
}
|
|
if len(values) == 0 {
|
|
return ""
|
|
}
|
|
data, err := json.Marshal(values)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
return string(data)
|
|
}
|
|
|
|
func migratedProviderComposerProfile(provider string) (providerregistry.ComposerProfileDescriptor, bool) {
|
|
descriptor, ok := providerregistry.Find(provider)
|
|
if !ok {
|
|
return providerregistry.ComposerProfileDescriptor{}, false
|
|
}
|
|
return descriptor.ComposerProfile, true
|
|
}
|