返回 DeepSeek-Reasonix
live.go
根目录 / internal / capdiag / live.go
1 package capdiag
2
3 import (
4 "context"
5 "fmt"
6 "os/exec"
7 "strings"
8 "time"
9
10 "reasonix/internal/boot"
11 "reasonix/internal/config"
12 "reasonix/internal/plugin"
13 )
14
15 func lookPath(cmd string) (string, error) {
16 return exec.LookPath(cmd)
17 }
18
19 // probeLiveMCP starts automatic-intent servers in an isolated Host, records
20 // connection results, and always closes the Host (including stdio children).
21 // Persistence (startup stats / schema cache) is disabled so --live stays
22 // free of cache/state side effects under Reasonix home.
23 func probeLiveMCP(rep *MCPReport, cfg *config.Config, root, home, reasonixHome string, timeout time.Duration) []Issue {
24 var issues []Issue
25 if cfg == nil {
26 return issues
27 }
28 if timeout <= 0 {
29 timeout = DefaultLiveTimeout
30 }
31 if timeout < MinLiveTimeout {
32 timeout = MinLiveTimeout
33 }
34 if timeout > MaxLiveTimeout {
35 timeout = MaxLiveTimeout
36 }
37
38 // Only probe servers that would start on their own; a project-declared one
39 // the user has not enabled must not start here either.
40 enabled := enabledMCPNames(cfg, root, reasonixHome)
41 var auto []config.PluginEntry
42 for _, p := range cfg.Plugins {
43 if enabled[p.Name] {
44 auto = append(auto, p)
45 } else {
46 for i := range rep.Servers {
47 if rep.Servers[i].Name == p.Name {
48 rep.Servers[i].RuntimeStatus = "skipped"
49 }
50 }
51 }
52 }
53 if len(auto) == 0 {
54 return issues
55 }
56
57 specs := boot.PluginSpecsForRoot(auto, root)
58 ctx, cancel := context.WithTimeout(context.Background(), timeout*time.Duration(len(specs))+timeout)
59 defer cancel()
60
61 host, _, _ := plugin.Start(ctx, specs, plugin.StartPolicy{
62 PerPluginTimeout: timeout,
63 Concurrency: 4,
64 AbortOnError: false,
65 SkipPersistence: true,
66 })
67 if host == nil {
68 return issues
69 }
70 defer host.Close()
71
72 byName := map[string]int{}
73 for i, s := range rep.Servers {
74 byName[s.Name] = i
75 }
76
77 connected := map[string]bool{}
78 for _, s := range host.Servers() {
79 rep.bindings = append(rep.bindings, s.ToolBindings...)
80 connected[s.Name] = true
81 tools := make([]MCPToolInfo, 0, len(s.ToolList))
82 for _, t := range s.ToolList {
83 tools = append(tools, MCPToolInfo{Name: t.Name, ReadOnlyHint: t.ReadOnlyHint, DestructiveHint: t.DestructiveHint})
84 }
85 if i, ok := byName[s.Name]; ok {
86 rep.Servers[i].RuntimeStatus = "probed"
87 rep.Servers[i].ToolCount = s.Tools
88 rep.Servers[i].Tools = tools
89 } else {
90 rep.Servers = append(rep.Servers, MCPServerInfo{
91 Name: s.Name, Transport: s.Transport, RuntimeStatus: "probed",
92 ToolCount: s.Tools, Tools: tools, StartIntent: "automatic",
93 })
94 }
95 if s.HasTools && s.Tools == 0 {
96 issues = append(issues, Issue{
97 Severity: "warning", Code: "mcp.no_tools", Subsystem: "mcp",
98 Name: s.Name, Message: "live probe: MCP server connected but exposes no tools",
99 Remediation: "Check server configuration and authentication",
100 SettingsTab: "mcp",
101 })
102 }
103 }
104 for _, f := range host.Failures() {
105 errText := sanitizeErrTextWithPaths(f.Error, root, home, reasonixHome)
106 if i, ok := byName[f.Name]; ok {
107 rep.Servers[i].RuntimeStatus = "failed"
108 rep.Servers[i].Error = errText
109 rep.Servers[i].StartupStage = f.Stage
110 rep.Servers[i].StartupElapsedMS = f.Elapsed.Milliseconds()
111 rep.Servers[i].Stderr = sanitizeErrTextWithPaths(f.Stderr, root, home, reasonixHome)
112 }
113 issues = append(issues, Issue{
114 Severity: "error", Code: "mcp.start_failed", Subsystem: "mcp",
115 Name: f.Name, Message: "live probe failed: " + errText,
116 Remediation: "Fix command/URL/auth; re-run with --live after changes",
117 SettingsTab: "mcp",
118 })
119 }
120 for _, p := range auto {
121 if connected[p.Name] {
122 continue
123 }
124 if i, ok := byName[p.Name]; ok {
125 if rep.Servers[i].RuntimeStatus == "failed" {
126 continue
127 }
128 if rep.Servers[i].RuntimeStatus == "" {
129 rep.Servers[i].RuntimeStatus = "failed"
130 rep.Servers[i].Error = "no connection result within timeout"
131 issues = append(issues, Issue{
132 Severity: "error", Code: "mcp.start_failed", Subsystem: "mcp",
133 Name: p.Name, Message: fmt.Sprintf("live probe: no result within %s", timeout),
134 Remediation: "Increase --timeout or fix a hanging MCP server",
135 SettingsTab: "mcp",
136 })
137 }
138 }
139 }
140 return issues
141 }
142
143 // HasErrorSeverity reports whether the report contains any error-level issue.
144 func HasErrorSeverity(r Report) bool {
145 for _, is := range r.Issues {
146 if is.Severity == "error" {
147 return true
148 }
149 }
150 return false
151 }
152
153 // LiveWarningMessage is printed to stderr before CLI --live starts MCP.
154 func LiveWarningMessage() string {
155 return strings.TrimSpace(`warning: --live will start third-party MCP servers in an isolated process.
156 They may access the network and receive configured environment variables and headers.
157 Tools are not registered into the agent registry. Startup stats/schema cache writes are disabled.
158 Host is always closed after the probe.`)
159 }
160
161 // enabledMCPNames resolves which servers start on their own, reading the
162 // activation store under reasonixHome.
163 func enabledMCPNames(cfg *config.Config, root, reasonixHome string) map[string]bool {
164 store := config.DefaultMCPActivationStore()
165 if strings.TrimSpace(reasonixHome) != "" {
166 store = config.NewMCPActivationStore(reasonixHome)
167 }
168 out := map[string]bool{}
169 for _, p := range cfg.EnabledPlugins(root, store) {
170 out[p.Name] = true
171 }
172 return out
173 }
174
175 func startIntent(enabled bool) string {
176 if enabled {
177 return "automatic"
178 }
179 return "off"
180 }
181
181 lines GO