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

380 lines
13 KiB
Go

package runtimeprep
import (
"context"
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
)
const tuttiAgentExtraSkillRootsEnv = "TUTTI_AGENT_EXTRA_SKILL_ROOTS_JSON"
// TuttiAgentPreparer materializes the session-scoped TUTTI_AGENT_HOME for the
// tutti-agent provider. Account-token bootstrap remains a host responsibility
// and can be injected through BeforePrepare.
type TuttiAgentPreparer struct {
BeforePrepare func(context.Context, PrepareInput)
ResolveAuthSource func(context.Context, PrepareInput) (string, error)
StableSkillBundleRoot string
StableSystemSkillBundleRoot string
AuthProjector AuthFileProjector
}
func (TuttiAgentPreparer) Provider() string {
return "tutti-agent"
}
func (p TuttiAgentPreparer) Prepare(ctx context.Context, input ProviderPrepareInput) (result ProviderPrepareResult, err error) {
home := filepath.Join(input.RuntimeRoot, "tutti-agent-home")
logRuntimePrepareTrace("runtime_prepare.tutti_agent.entered", input.PrepareInput, nil)
if p.BeforePrepare != nil {
p.BeforePrepare(ctx, input.PrepareInput)
}
authSource := ""
authSourceConfigured := p.ResolveAuthSource != nil
if authSourceConfigured {
resolved, err := p.ResolveAuthSource(ctx, input.PrepareInput)
if err != nil {
return ProviderPrepareResult{}, fmt.Errorf("resolve tutti-agent auth source: %w", err)
}
authSource = strings.TrimSpace(resolved)
}
cleanup, err := prepareTuttiAgentHomeWithProjector(ctx, home, input.PrepareInput, authSource, authSourceConfigured, p.AuthProjector)
if err != nil {
return ProviderPrepareResult{}, err
}
defer func() {
if err != nil && cleanup != nil {
err = errors.Join(err, cleanup(ctx))
}
}()
var extraSkillRoots []string
if !input.SkipSkills {
extraSkillRoots, err = connectorSkillRoots(input.ConnectorRoutingHints)
if err != nil {
return ProviderPrepareResult{}, err
}
if strings.TrimSpace(p.StableSkillBundleRoot) == "" {
if _, err := installProviderNativeSkillsSessionScoped(filepath.Join(home, "skills"), input.PrepareInput); err != nil {
return ProviderPrepareResult{}, fmt.Errorf("install tutti-agent native skills: %w", err)
}
} else {
root, err := materializeStableProviderSkills(p.StableSkillBundleRoot, input.PrepareInput)
if err != nil {
return ProviderPrepareResult{}, fmt.Errorf("materialize tutti-agent stable skills: %w", err)
}
if root != "" {
extraSkillRoots = append([]string{root}, extraSkillRoots...)
}
}
}
logRuntimePrepareTrace("runtime_prepare.tutti_agent.home_prepared", input.PrepareInput, nil)
instructionsPath := filepath.Join(home, "AGENTS.md")
policy, err := tuttiCLIPolicy(input.PrepareInput)
if err != nil {
return ProviderPrepareResult{}, err
}
writeResult, err := input.Store.WriteManagedBlock(instructionsPath, policy)
if err != nil {
return ProviderPrepareResult{}, err
}
if err := ensureRTKInstructionsReferenceFirst(instructionsPath, input.PrepareInput); err != nil {
return ProviderPrepareResult{}, err
}
if input.Manifest != nil {
input.Manifest.RecordManagedFile(instructionsPath, "provider-instructions", writeResult.Created)
input.Manifest.RecordManagedFile(home, "tutti-agent-home", true)
}
logRuntimePrepareTrace("runtime_prepare.tutti_agent.resolved", input.PrepareInput, nil)
env := []string{"TUTTI_AGENT_HOME=" + home}
if len(extraSkillRoots) > 0 {
encodedRoots, err := json.Marshal(extraSkillRoots)
if err != nil {
return ProviderPrepareResult{}, fmt.Errorf("encode tutti-agent extra skill roots: %w", err)
}
env = append(env, tuttiAgentExtraSkillRootsEnv+"="+string(encodedRoots))
}
if !input.SkipSkills {
if root := strings.TrimSpace(p.StableSystemSkillBundleRoot); root != "" {
root = filepath.Clean(root)
if !filepath.IsAbs(root) {
return ProviderPrepareResult{}, fmt.Errorf("tutti-agent stable system skill bundle root must be absolute")
}
env = append(env, "TUTTI_AGENT_STABLE_SYSTEM_SKILLS_ROOT="+root)
}
}
if input.ModelEndpoint.supportsCodex() {
env = append(env, codexModelPlanAPIKeyEnv+"="+input.ModelEndpoint.APIKey)
}
return ProviderPrepareResult{
Cwd: input.Cwd,
Env: env,
Cleanup: cleanup,
}, nil
}
func connectorSkillRoots(hints []ConnectorRoutingHint) ([]string, error) {
roots := make([]string, 0, len(hints))
seen := make(map[string]struct{}, len(hints))
for _, hint := range hints {
root := strings.TrimSpace(hint.SkillRoot)
if root == "" {
continue
}
root = filepath.Clean(root)
if !filepath.IsAbs(root) {
return nil, fmt.Errorf("connector %q Skill root must be absolute", hint.ConnectorKey)
}
info, err := os.Lstat(root)
if err != nil {
return nil, fmt.Errorf("inspect connector %q Skill root: %w", hint.ConnectorKey, err)
}
if info.Mode()&os.ModeSymlink != 0 || !info.IsDir() {
return nil, fmt.Errorf("connector %q Skill root must be a directory, not a symlink", hint.ConnectorKey)
}
if _, exists := seen[root]; exists {
continue
}
seen[root] = struct{}{}
roots = append(roots, root)
}
return roots, nil
}
// PrepareTuttiAgentHome materializes a TUTTI_AGENT_HOME with the user's auth
// exposed and session-safe host policy. Provider and model selection remain
// owned by tutti-agent and the per-session launch request. Session Skills are
// installed only by TuttiAgentPreparer after capabilities resolve.
func PrepareTuttiAgentHome(home string, input PrepareInput) error {
return prepareTuttiAgentHome(home, input, "", false)
}
func prepareTuttiAgentHome(home string, input PrepareInput, authSource string, authSourceConfigured bool) error {
_, err := prepareTuttiAgentHomeWithProjector(context.Background(), home, input, authSource, authSourceConfigured, nil)
return err
}
func prepareTuttiAgentHomeWithProjector(ctx context.Context, home string, input PrepareInput, authSource string, authSourceConfigured bool, projector AuthFileProjector) (func(context.Context) error, error) {
if err := os.MkdirAll(home, 0o700); err != nil {
return nil, fmt.Errorf("create tutti-agent home: %w", err)
}
cleanup, err := exposeUserTuttiAgentFilesWithProjector(ctx, home, authSource, authSourceConfigured, projector)
if err != nil {
return nil, err
}
if err := ensureTuttiAgentSessionConfig(filepath.Join(home, "config.toml"), input); err != nil {
return nil, err
}
return cleanup, nil
}
func exposeUserTuttiAgentFilesWithProjector(ctx context.Context, home string, explicitAuthSource string, explicitAuthSourceConfigured bool, projector AuthFileProjector) (func(context.Context) error, error) {
if explicitAuthSourceConfigured {
if explicitAuthSource == "" {
return nil, nil
}
if !filepath.IsAbs(explicitAuthSource) {
return nil, fmt.Errorf("tutti-agent auth source must be absolute")
}
return exposeTuttiAgentAuthWithProjector(ctx, home, filepath.Clean(explicitAuthSource), true, projector)
}
userHome, err := os.UserHomeDir()
if err != nil || strings.TrimSpace(userHome) == "" {
return nil, nil
}
userAgentHome := filepath.Join(userHome, ".tutti-agent")
source := filepath.Join(userAgentHome, "auth.json")
cleanup, err := exposeTuttiAgentAuthWithProjector(ctx, home, source, false, projector)
if err != nil {
return nil, err
}
target := filepath.Join(home, "config.toml")
if _, err := os.Lstat(target); os.IsNotExist(err) {
userConfig := filepath.Join(userAgentHome, "config.toml")
if _, err := os.Stat(userConfig); err == nil {
if err := copyFile(userConfig, target, 0o600); err != nil {
return nil, fmt.Errorf("copy tutti-agent config: %w", err)
}
}
}
return cleanup, nil
}
func exposeTuttiAgentAuthWithProjector(ctx context.Context, home string, source string, allowMissingSource bool, projector AuthFileProjector) (func(context.Context) error, error) {
if !allowMissingSource {
if _, err := os.Stat(source); err != nil {
if os.IsNotExist(err) {
return nil, nil
}
return nil, fmt.Errorf("stat tutti-agent auth source: %w", err)
}
}
target := filepath.Join(home, "auth.json")
if info, err := os.Lstat(target); err == nil {
if info.Mode()&os.ModeSymlink != 0 {
current, readErr := os.Readlink(target)
if readErr != nil {
return nil, fmt.Errorf("read tutti-agent auth target: %w", readErr)
}
if current == source {
return nil, nil
}
return nil, fmt.Errorf("tutti-agent auth target already links to a different source")
}
if projector != nil {
cleanup, projectErr := projector.Project(ctx, AuthFileProjection{SourcePath: source, TargetPath: target, LockSourcePath: filepath.Join(filepath.Dir(source), ".refresh.lock"), LockTargetPath: filepath.Join(home, ".refresh.lock")})
if projectErr != nil {
return nil, fmt.Errorf("recover tutti-agent auth projection: %w", projectErr)
}
return cleanup, nil
}
// Legacy embedders without a projector retain their existing regular
// run-scoped view.
return nil, nil
} else if !os.IsNotExist(err) {
return nil, fmt.Errorf("inspect tutti-agent auth target: %w", err)
}
if projector != nil {
cleanup, err := projector.Project(ctx, AuthFileProjection{SourcePath: source, TargetPath: target, LockSourcePath: filepath.Join(filepath.Dir(source), ".refresh.lock"), LockTargetPath: filepath.Join(home, ".refresh.lock")})
if err != nil {
return nil, fmt.Errorf("expose tutti-agent auth.json: %w", err)
}
return cleanup, nil
}
if err := exposeCodexFile(source, target, 0o600); err != nil {
return nil, fmt.Errorf("expose tutti-agent auth.json: %w", err)
}
return nil, nil
}
func ensureTuttiAgentSessionConfig(configPath string, input PrepareInput) error {
contentBytes, err := os.ReadFile(configPath)
if err != nil && !os.IsNotExist(err) {
return fmt.Errorf("read tutti-agent config: %w", err)
}
next, changed := codexConfigWithProjectRootMarkersDisabled(string(contentBytes))
if tuttiNext, tuttiChanged := codexConfigWithTuttiConversationDetailMode(next, input.ConversationDetailMode); tuttiChanged {
next = tuttiNext
changed = true
}
if detailModeNext, detailModeChanged := codexConfigWithConversationDetailModeInstructions(next, input.ConversationDetailMode); detailModeChanged {
next = detailModeNext
changed = true
}
if cleaned, cleanedChanged := tuttiAgentConfigWithoutLegacyPinnedProvider(next); cleanedChanged {
next = cleaned
changed = true
}
if planNext, planChanged := codexConfigWithModelPlanEndpoint(next, input.ModelEndpoint); planChanged {
next = planNext
changed = true
}
if mcpNext, mcpChanged := codexConfigWithConnectorMCP(next, input.MCPServers); mcpChanged {
next = mcpNext
changed = true
}
// Tutti Agent uses the same Codex-derived app-server sandbox runtime as
// Codex, but it is launched by Tutti's non-elevated desktop daemon. Keep
// the Windows implementation aligned with Codex session homes so an
// interactive UAC setup helper cannot block app-server startup.
if windowsSandboxNext, windowsSandboxChanged := codexConfigWithTuttiWindowsSandbox(next); windowsSandboxChanged {
next = windowsSandboxNext
changed = true
}
if !changed {
return nil
}
if err := os.WriteFile(configPath, []byte(next), 0o600); err != nil {
return fmt.Errorf("write tutti-agent config: %w", err)
}
return nil
}
// tuttiAgentConfigWithoutLegacyPinnedProvider removes only the exact
// host-generated provider signature shipped by older runtimeprep releases.
// User-owned provider/model settings are otherwise preserved.
func tuttiAgentConfigWithoutLegacyPinnedProvider(content string) (string, bool) {
normalized := strings.ReplaceAll(content, "\r\n", "\n")
lines := strings.Split(normalized, "\n")
rootProviderLine := -1
rootModelLine := -1
legacySectionStart := -1
legacySectionEnd := len(lines)
firstSection := len(lines)
for index, line := range lines {
trimmed := strings.TrimSpace(line)
if strings.HasPrefix(trimmed, "[") && strings.HasSuffix(trimmed, "]") {
if firstSection == len(lines) {
firstSection = index
}
if trimmed == `[model_providers.tutti-llm]` {
if legacySectionStart >= 0 {
return content, false
}
legacySectionStart = index
continue
}
if legacySectionStart >= 0 && legacySectionEnd == len(lines) {
legacySectionEnd = index
}
continue
}
if index >= firstSection {
continue
}
switch trimmed {
case `model_provider = "tutti-llm"`:
if rootProviderLine >= 0 {
return content, false
}
rootProviderLine = index
case `model = "gpt-5.4"`:
if rootModelLine >= 0 {
return content, false
}
rootModelLine = index
}
}
if rootProviderLine < 0 || rootModelLine < 0 || legacySectionStart < 0 {
return content, false
}
legacyKeys := map[string]bool{
`name = "Tutti LLM"`: false,
`base_url = "https://llm-api.tutti.sh/v1"`: false,
`wire_api = "responses"`: false,
}
for _, line := range lines[legacySectionStart+1 : legacySectionEnd] {
trimmed := strings.TrimSpace(line)
if trimmed == "" || strings.HasPrefix(trimmed, "#") {
continue
}
if _, ok := legacyKeys[trimmed]; !ok || legacyKeys[trimmed] {
return content, false
}
legacyKeys[trimmed] = true
}
for _, found := range legacyKeys {
if !found {
return content, false
}
}
result := make([]string, 0, len(lines))
for index, line := range lines {
if index == rootProviderLine || index == rootModelLine {
continue
}
if index >= legacySectionStart && index < legacySectionEnd {
continue
}
result = append(result, line)
}
next := strings.Join(result, "\n")
if strings.HasSuffix(normalized, "\n") && !strings.HasSuffix(next, "\n") {
next += "\n"
}
return next, true
}