Skip to content

Commit 92e9839

Browse files
author
SqlRush
committed
Send LSP initialize handshakes
1 parent d7621a9 commit 92e9839

5 files changed

Lines changed: 475 additions & 4 deletions

File tree

docs/cc-100-roadmap.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1456,7 +1456,7 @@ M7 补充:terminal input parser 和 configurable keybinding name parser 现在
14561456
- Chrome/computer-use/voice/native integrations。
14571457
- enterprise/gated/platform-specific behavior。
14581458

1459-
当前状态:已新增 `advanced` settings gate 地基,覆盖 bridge/LSP/telemetry/Chrome/voice/computer-use/native integrations 的独立 bool 开关解析、settings merge、headless `/config show advanced` 和 `/config search` 审计;`advanced.telemetry=true` 时会在 session 目录写入安全摘要 JSONL 诊断事件,记录事件类型、session/model、tool/progress keys、token/compact/error 摘要,不写入用户/助手正文或工具结果内容,并已有安全 JSONL 读取、类型/模型过滤、汇总统计和 headless `/status show telemetry` 地基;`advanced.lsp=true` 时才向模型暴露只读 `LSPDiagnostics` 工具,用于读取 session-scoped diagnostics snapshot,并支持 file/severity/limit 过滤,底层已能解析 LSP `textDocument/publishDiagnostics` params/notification payload、按文件替换 snapshot、按 LSP 空 diagnostics 语义清空旧文件诊断;同时会写出 session-scoped LSP manager status,按默认 server definitions 解析 workspace/root marker/file extension 命中情况,将已匹配但未启动的 server 标为 `not_started`,并可通过 headless `/status show lsp` 审计 diagnostics 与 manager runtime state;LSP Content-Length framed JSON-RPC reader/writer 与 diagnostics stream processor 已能持续消费 `textDocument/publishDiagnostics` notification 并更新 session snapshot;LSP server process lifecycle API 已可启动配置命令、消费 stdout framed diagnostics stream,并把 `running`/`exited`/`failed`、PID 和时间戳写回 manager status;`advanced.bridge=true` 时会写出 session-scoped bridge manifest,列出 bridge-safe slash/local command 元数据,提供 command/display/alias 安全解析,并可通过 headless `/status show bridge` 审计;bridge direct HTTP handler 已可复用同一 manifest/registry,提供 `/health`、`/manifest`、`/resolve` 和 `/execute` JSON 端点,执行前强制 bridge-safe 白名单并默认不回传展开后的 prompt 正文;bridge loopback-only direct listener 已有可嵌入启动/关闭 API,默认 `127.0.0.1:0`、拒绝非 loopback 地址,并支持可选 bearer/`X-Bridge-Token` token guard;`advanced.nativeIntegrations=true` 时会写出 session-scoped native capability manifest,记录平台和安全能力状态,并可通过 headless `/status show native` 审计;任一 `advanced.chrome`/`advanced.voice`/`advanced.computerUse=true` 时会写出 session-scoped integrations manifest,将 Chrome/voice/computer-use 的 enabled 与 `runtime_state` 分开记录,当前启用项明确标为 `not_wired`,并可通过 headless `/status show integrations` 审计;未启用时仍不注册或泄露 gated 工具 schema。实际 bridge websocket、session websocket/direct connect 外部接入、LSP initialize/didOpen 编排和 runner 自动启动、telemetry exporter/tracing、Chrome/voice/computer-use/native runtime 仍未完成。
1459+
当前状态:已新增 `advanced` settings gate 地基,覆盖 bridge/LSP/telemetry/Chrome/voice/computer-use/native integrations 的独立 bool 开关解析、settings merge、headless `/config show advanced` 和 `/config search` 审计;`advanced.telemetry=true` 时会在 session 目录写入安全摘要 JSONL 诊断事件,记录事件类型、session/model、tool/progress keys、token/compact/error 摘要,不写入用户/助手正文或工具结果内容,并已有安全 JSONL 读取、类型/模型过滤、汇总统计和 headless `/status show telemetry` 地基;`advanced.lsp=true` 时才向模型暴露只读 `LSPDiagnostics` 工具,用于读取 session-scoped diagnostics snapshot,并支持 file/severity/limit 过滤,底层已能解析 LSP `textDocument/publishDiagnostics` params/notification payload、按文件替换 snapshot、按 LSP 空 diagnostics 语义清空旧文件诊断;同时会写出 session-scoped LSP manager status,按默认 server definitions 解析 workspace/root marker/file extension 命中情况,将已匹配但未启动的 server 标为 `not_started`,并可通过 headless `/status show lsp` 审计 diagnostics 与 manager runtime state;LSP Content-Length framed JSON-RPC reader/writer 与 diagnostics stream processor 已能持续消费 `textDocument/publishDiagnostics` notification 并更新 session snapshot;LSP server process lifecycle API 已可启动配置命令、消费 stdout framed diagnostics stream,并把 `running`/`exited`/`failed`、PID 和时间戳写回 manager status;LSP client handshake send path 已可向 server stdin 写入 `initialize`、`initialized` 和 `textDocument/didOpen` framed JSON-RPC,支持 root/client/document metadata 和 file URI/language 默认推断;`advanced.bridge=true` 时会写出 session-scoped bridge manifest,列出 bridge-safe slash/local command 元数据,提供 command/display/alias 安全解析,并可通过 headless `/status show bridge` 审计;bridge direct HTTP handler 已可复用同一 manifest/registry,提供 `/health`、`/manifest`、`/resolve` 和 `/execute` JSON 端点,执行前强制 bridge-safe 白名单并默认不回传展开后的 prompt 正文;bridge loopback-only direct listener 已有可嵌入启动/关闭 API,默认 `127.0.0.1:0`、拒绝非 loopback 地址,并支持可选 bearer/`X-Bridge-Token` token guard;`advanced.nativeIntegrations=true` 时会写出 session-scoped native capability manifest,记录平台和安全能力状态,并可通过 headless `/status show native` 审计;任一 `advanced.chrome`/`advanced.voice`/`advanced.computerUse=true` 时会写出 session-scoped integrations manifest,将 Chrome/voice/computer-use 的 enabled 与 `runtime_state` 分开记录,当前启用项明确标为 `not_wired`,并可通过 headless `/status show integrations` 审计;未启用时仍不注册或泄露 gated 工具 schema。实际 bridge websocket、session websocket/direct connect 外部接入、LSP initialize response negotiation 和 runner 自动启动、telemetry exporter/tracing、Chrome/voice/computer-use/native runtime 仍未完成。
14601460

14611461
## Recommended Next Steps
14621462

docs/claude-code-go-rewrite-plan.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -745,7 +745,7 @@ test/parity/ # golden tests against TS/official behavior
745745
- 每个 gated feature 独立开关测试。
746746
- 不启用 feature 时二进制行为和可见 schema 不泄露 gated 工具/命令。
747747

748-
- 本轮补充:新增 `advanced` settings gate 地基,覆盖 bridge/LSP/telemetry/Chrome/voice/computer-use/native integrations 的独立 bool 开关解析、settings merge、headless `/config show advanced` 和 `/config search` 审计;`advanced.telemetry=true` 时会在 session 目录写入安全摘要 JSONL 诊断事件,记录事件类型、session/model、tool/progress keys、token/compact/error 摘要,不写入用户/助手正文或工具结果内容,并已有安全 JSONL 读取、类型/模型过滤、汇总统计和 headless `/status show telemetry` 地基;`advanced.lsp=true` 时才向模型暴露只读 `LSPDiagnostics` 工具,用于读取 session-scoped diagnostics snapshot,并支持 file/severity/limit 过滤,底层已能解析 LSP `textDocument/publishDiagnostics` params/notification payload、按文件替换 snapshot、按 LSP 空 diagnostics 语义清空旧文件诊断;同时会写出 session-scoped LSP manager status,按默认 server definitions 解析 workspace/root marker/file extension 命中情况,将已匹配但未启动的 server 标为 `not_started`,并可通过 headless `/status show lsp` 审计 diagnostics 与 manager runtime state;LSP Content-Length framed JSON-RPC reader/writer 与 diagnostics stream processor 已能持续消费 `textDocument/publishDiagnostics` notification 并更新 session snapshot;LSP server process lifecycle API 已可启动配置命令、消费 stdout framed diagnostics stream,并把 `running`/`exited`/`failed`、PID 和时间戳写回 manager status;`advanced.bridge=true` 时会写出 session-scoped bridge manifest,列出 bridge-safe slash/local command 元数据,提供 command/display/alias 安全解析,并可通过 headless `/status show bridge` 审计;bridge direct HTTP handler 已可复用同一 manifest/registry,提供 `/health`、`/manifest`、`/resolve` 和 `/execute` JSON 端点,执行前强制 bridge-safe 白名单并默认不回传展开后的 prompt 正文;bridge loopback-only direct listener 已有可嵌入启动/关闭 API,默认 `127.0.0.1:0`、拒绝非 loopback 地址,并支持可选 bearer/`X-Bridge-Token` token guard;`advanced.nativeIntegrations=true` 时会写出 session-scoped native capability manifest,记录平台和安全能力状态,并可通过 headless `/status show native` 审计;任一 `advanced.chrome`/`advanced.voice`/`advanced.computerUse=true` 时会写出 session-scoped integrations manifest,将 Chrome/voice/computer-use 的 enabled 与 `runtime_state` 分开记录,当前启用项明确标为 `not_wired`,并可通过 headless `/status show integrations` 审计;未启用时仍不注册或泄露 gated 工具 schema。实际 bridge websocket、session websocket/direct connect 外部接入、LSP initialize/didOpen 编排和 runner 自动启动、telemetry exporter/tracing、Chrome/voice/computer-use/native runtime 仍未完成。
748+
- 本轮补充:新增 `advanced` settings gate 地基,覆盖 bridge/LSP/telemetry/Chrome/voice/computer-use/native integrations 的独立 bool 开关解析、settings merge、headless `/config show advanced` 和 `/config search` 审计;`advanced.telemetry=true` 时会在 session 目录写入安全摘要 JSONL 诊断事件,记录事件类型、session/model、tool/progress keys、token/compact/error 摘要,不写入用户/助手正文或工具结果内容,并已有安全 JSONL 读取、类型/模型过滤、汇总统计和 headless `/status show telemetry` 地基;`advanced.lsp=true` 时才向模型暴露只读 `LSPDiagnostics` 工具,用于读取 session-scoped diagnostics snapshot,并支持 file/severity/limit 过滤,底层已能解析 LSP `textDocument/publishDiagnostics` params/notification payload、按文件替换 snapshot、按 LSP 空 diagnostics 语义清空旧文件诊断;同时会写出 session-scoped LSP manager status,按默认 server definitions 解析 workspace/root marker/file extension 命中情况,将已匹配但未启动的 server 标为 `not_started`,并可通过 headless `/status show lsp` 审计 diagnostics 与 manager runtime state;LSP Content-Length framed JSON-RPC reader/writer 与 diagnostics stream processor 已能持续消费 `textDocument/publishDiagnostics` notification 并更新 session snapshot;LSP server process lifecycle API 已可启动配置命令、消费 stdout framed diagnostics stream,并把 `running`/`exited`/`failed`、PID 和时间戳写回 manager status;LSP client handshake send path 已可向 server stdin 写入 `initialize`、`initialized` 和 `textDocument/didOpen` framed JSON-RPC,支持 root/client/document metadata 和 file URI/language 默认推断;`advanced.bridge=true` 时会写出 session-scoped bridge manifest,列出 bridge-safe slash/local command 元数据,提供 command/display/alias 安全解析,并可通过 headless `/status show bridge` 审计;bridge direct HTTP handler 已可复用同一 manifest/registry,提供 `/health`、`/manifest`、`/resolve` 和 `/execute` JSON 端点,执行前强制 bridge-safe 白名单并默认不回传展开后的 prompt 正文;bridge loopback-only direct listener 已有可嵌入启动/关闭 API,默认 `127.0.0.1:0`、拒绝非 loopback 地址,并支持可选 bearer/`X-Bridge-Token` token guard;`advanced.nativeIntegrations=true` 时会写出 session-scoped native capability manifest,记录平台和安全能力状态,并可通过 headless `/status show native` 审计;任一 `advanced.chrome`/`advanced.voice`/`advanced.computerUse=true` 时会写出 session-scoped integrations manifest,将 Chrome/voice/computer-use 的 enabled 与 `runtime_state` 分开记录,当前启用项明确标为 `not_wired`,并可通过 headless `/status show integrations` 审计;未启用时仍不注册或泄露 gated 工具 schema。实际 bridge websocket、session websocket/direct connect 外部接入、LSP initialize response negotiation 和 runner 自动启动、telemetry exporter/tracing、Chrome/voice/computer-use/native runtime 仍未完成。
749749

750750
### M12: Parity hardening
751751

internal/lsp/client.go

Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
package lsp
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"io"
7+
"net/url"
8+
"os"
9+
"path/filepath"
10+
"strings"
11+
)
12+
13+
const defaultInitializeID = 1
14+
15+
type ServerHandshakeOptions struct {
16+
InitializeID int
17+
ProcessID int
18+
RootURI string
19+
RootPath string
20+
ClientName string
21+
ClientVersion string
22+
Trace string
23+
Documents []OpenDocument
24+
}
25+
26+
type OpenDocument struct {
27+
URI string
28+
FilePath string
29+
LanguageID string
30+
Version int
31+
Text string
32+
}
33+
34+
type textDocumentItem struct {
35+
URI string `json:"uri"`
36+
LanguageID string `json:"languageId"`
37+
Version int `json:"version"`
38+
Text string `json:"text"`
39+
}
40+
41+
type jsonRPCMessage struct {
42+
JSONRPC string `json:"jsonrpc"`
43+
ID int `json:"id,omitempty"`
44+
Method string `json:"method"`
45+
Params any `json:"params,omitempty"`
46+
}
47+
48+
func (p *ServerProcess) InitializeAndOpen(ctx context.Context, opts ServerHandshakeOptions) error {
49+
if p == nil || p.stdin == nil {
50+
return os.ErrInvalid
51+
}
52+
return WriteInitializeAndOpen(ctx, p.stdin, opts)
53+
}
54+
55+
func WriteInitializeAndOpen(ctx context.Context, w io.Writer, opts ServerHandshakeOptions) error {
56+
if ctx == nil {
57+
ctx = context.Background()
58+
}
59+
if w == nil {
60+
return os.ErrInvalid
61+
}
62+
if err := WriteInitializeRequest(ctx, w, opts); err != nil {
63+
return err
64+
}
65+
if err := WriteInitializedNotification(ctx, w); err != nil {
66+
return err
67+
}
68+
for _, doc := range opts.Documents {
69+
if err := WriteDidOpenNotification(ctx, w, doc); err != nil {
70+
return err
71+
}
72+
}
73+
return nil
74+
}
75+
76+
func WriteInitializeRequest(ctx context.Context, w io.Writer, opts ServerHandshakeOptions) error {
77+
if err := contextErr(ctx); err != nil {
78+
return err
79+
}
80+
if w == nil {
81+
return os.ErrInvalid
82+
}
83+
id := opts.InitializeID
84+
if id == 0 {
85+
id = defaultInitializeID
86+
}
87+
processID := opts.ProcessID
88+
if processID == 0 {
89+
processID = os.Getpid()
90+
}
91+
clientName := strings.TrimSpace(opts.ClientName)
92+
if clientName == "" {
93+
clientName = "ccgo"
94+
}
95+
params := map[string]any{
96+
"processId": processID,
97+
"clientInfo": map[string]any{
98+
"name": clientName,
99+
"version": strings.TrimSpace(opts.ClientVersion),
100+
},
101+
"capabilities": defaultClientCapabilities(),
102+
}
103+
if rootURI := strings.TrimSpace(opts.RootURI); rootURI != "" {
104+
params["rootUri"] = rootURI
105+
}
106+
if rootPath := strings.TrimSpace(opts.RootPath); rootPath != "" {
107+
params["rootPath"] = rootPath
108+
}
109+
if trace := strings.TrimSpace(opts.Trace); trace != "" {
110+
params["trace"] = trace
111+
}
112+
return writeFramedJSON(w, jsonRPCMessage{
113+
JSONRPC: "2.0",
114+
ID: id,
115+
Method: "initialize",
116+
Params: params,
117+
})
118+
}
119+
120+
func WriteInitializedNotification(ctx context.Context, w io.Writer) error {
121+
if err := contextErr(ctx); err != nil {
122+
return err
123+
}
124+
if w == nil {
125+
return os.ErrInvalid
126+
}
127+
return writeFramedJSON(w, jsonRPCMessage{
128+
JSONRPC: "2.0",
129+
Method: "initialized",
130+
Params: map[string]any{},
131+
})
132+
}
133+
134+
func WriteDidOpenNotification(ctx context.Context, w io.Writer, doc OpenDocument) error {
135+
if err := contextErr(ctx); err != nil {
136+
return err
137+
}
138+
if w == nil {
139+
return os.ErrInvalid
140+
}
141+
item, err := normalizeOpenDocument(doc)
142+
if err != nil {
143+
return err
144+
}
145+
return writeFramedJSON(w, jsonRPCMessage{
146+
JSONRPC: "2.0",
147+
Method: "textDocument/didOpen",
148+
Params: map[string]any{
149+
"textDocument": item,
150+
},
151+
})
152+
}
153+
154+
func FileURIFromPath(path string) string {
155+
path = strings.TrimSpace(path)
156+
if path == "" {
157+
return ""
158+
}
159+
cleaned := filepath.Clean(path)
160+
if !filepath.IsAbs(cleaned) {
161+
if abs, err := filepath.Abs(cleaned); err == nil {
162+
cleaned = abs
163+
}
164+
}
165+
u := url.URL{Scheme: "file", Path: filepath.ToSlash(cleaned)}
166+
return u.String()
167+
}
168+
169+
func defaultClientCapabilities() map[string]any {
170+
return map[string]any{
171+
"textDocument": map[string]any{
172+
"publishDiagnostics": map[string]any{
173+
"relatedInformation": true,
174+
},
175+
},
176+
"workspace": map[string]any{
177+
"workspaceFolders": true,
178+
},
179+
}
180+
}
181+
182+
func normalizeOpenDocument(doc OpenDocument) (textDocumentItem, error) {
183+
uri := strings.TrimSpace(doc.URI)
184+
if uri == "" {
185+
uri = FileURIFromPath(doc.FilePath)
186+
}
187+
if uri == "" {
188+
return textDocumentItem{}, os.ErrInvalid
189+
}
190+
languageID := strings.TrimSpace(doc.LanguageID)
191+
if languageID == "" {
192+
languageID = languageIDFromPath(doc.FilePath)
193+
}
194+
if languageID == "" {
195+
languageID = "plaintext"
196+
}
197+
version := doc.Version
198+
if version == 0 {
199+
version = 1
200+
}
201+
return textDocumentItem{
202+
URI: uri,
203+
LanguageID: languageID,
204+
Version: version,
205+
Text: doc.Text,
206+
}, nil
207+
}
208+
209+
func languageIDFromPath(path string) string {
210+
switch strings.ToLower(filepath.Ext(strings.TrimSpace(path))) {
211+
case ".c", ".h":
212+
return "c"
213+
case ".cc", ".cpp", ".cxx", ".hh", ".hpp", ".hxx":
214+
return "cpp"
215+
case ".go":
216+
return "go"
217+
case ".js", ".jsx", ".mjs", ".cjs":
218+
return "javascript"
219+
case ".json":
220+
return "json"
221+
case ".lua":
222+
return "lua"
223+
case ".md", ".markdown":
224+
return "markdown"
225+
case ".py":
226+
return "python"
227+
case ".rs":
228+
return "rust"
229+
case ".ts", ".tsx":
230+
return "typescript"
231+
default:
232+
return ""
233+
}
234+
}
235+
236+
func writeFramedJSON(w io.Writer, value any) error {
237+
data, err := json.Marshal(value)
238+
if err != nil {
239+
return err
240+
}
241+
return WriteFramedMessage(w, data)
242+
}
243+
244+
func contextErr(ctx context.Context) error {
245+
if ctx == nil {
246+
return nil
247+
}
248+
return ctx.Err()
249+
}

0 commit comments

Comments
 (0)