Skip to content

Commit 6fc7e11

Browse files
author
SqlRush
committed
Add bridge direct handler
1 parent ee8d231 commit 6fc7e11

4 files changed

Lines changed: 492 additions & 2 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 语义清空旧文件诊断,并可通过 headless `/status show lsp` 审计;`advanced.bridge=true` 时会写出 session-scoped bridge manifest,列出 bridge-safe slash/local command 元数据,提供 command/display/alias 安全解析,并可通过 headless `/status show bridge` 审计;`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。实际 repl bridge server、session websocket/direct connect、LSP server manager、diagnostic producer、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 语义清空旧文件诊断,并可通过 headless `/status show lsp` 审计;`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 正文;`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 listener/websocket、session websocket/direct connect 外部接入、LSP server manager、diagnostic producer、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 语义清空旧文件诊断,并可通过 headless `/status show lsp` 审计;`advanced.bridge=true` 时会写出 session-scoped bridge manifest,列出 bridge-safe slash/local command 元数据,提供 command/display/alias 安全解析,并可通过 headless `/status show bridge` 审计;`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。实际 repl bridge server、session websocket/direct connect、LSP server manager、diagnostic producer、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 语义清空旧文件诊断,并可通过 headless `/status show lsp` 审计;`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 正文;`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 listener/websocket、session websocket/direct connect 外部接入、LSP server manager、diagnostic producer、telemetry exporter/tracing、Chrome/voice/computer-use/native runtime 仍未完成。
749749

750750
### M12: Parity hardening
751751

internal/bridge/direct.go

Lines changed: 278 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,278 @@
1+
package bridge
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"net/http"
7+
"strings"
8+
9+
"ccgo/internal/commands"
10+
"ccgo/internal/contracts"
11+
)
12+
13+
type DirectOptions struct {
14+
SessionID contracts.ID
15+
Manifest Manifest
16+
Registry commands.Registry
17+
}
18+
19+
type DirectHandler struct {
20+
sessionID contracts.ID
21+
manifest Manifest
22+
registry commands.Registry
23+
}
24+
25+
type DirectHealthResponse struct {
26+
OK bool `json:"ok"`
27+
SessionID contracts.ID `json:"session_id,omitempty"`
28+
Commands int `json:"commands"`
29+
}
30+
31+
type DirectCommandRequest struct {
32+
Command string `json:"command"`
33+
UUID contracts.ID `json:"uuid,omitempty"`
34+
}
35+
36+
type DirectResolveResponse struct {
37+
Allowed bool `json:"allowed"`
38+
Command *Command `json:"command,omitempty"`
39+
Name string `json:"name,omitempty"`
40+
Args string `json:"args,omitempty"`
41+
Reason string `json:"reason,omitempty"`
42+
}
43+
44+
type DirectExecuteResponse struct {
45+
Allowed bool `json:"allowed"`
46+
Handled bool `json:"handled"`
47+
Command *Command `json:"command,omitempty"`
48+
Name string `json:"name,omitempty"`
49+
Args string `json:"args,omitempty"`
50+
ShouldQuery bool `json:"should_query"`
51+
Model string `json:"model,omitempty"`
52+
AllowedTools []string `json:"allowed_tools,omitempty"`
53+
LocalResult *DirectLocalResult `json:"local_result,omitempty"`
54+
ResultText string `json:"result_text,omitempty"`
55+
Messages int `json:"messages"`
56+
Unknown bool `json:"unknown,omitempty"`
57+
Unsupported bool `json:"unsupported,omitempty"`
58+
Error string `json:"error,omitempty"`
59+
}
60+
61+
type DirectLocalResult struct {
62+
Type commands.LocalCommandResultType `json:"type"`
63+
HasValue bool `json:"has_value"`
64+
}
65+
66+
type DirectErrorResponse struct {
67+
Error string `json:"error"`
68+
}
69+
70+
func NewDirectHandler(opts DirectOptions) *DirectHandler {
71+
sessionID := opts.SessionID
72+
if sessionID == "" {
73+
sessionID = opts.Manifest.SessionID
74+
}
75+
return &DirectHandler{
76+
sessionID: sessionID,
77+
manifest: opts.Manifest,
78+
registry: opts.Registry,
79+
}
80+
}
81+
82+
func NewDirectHandlerFromSettings(sessionID contracts.ID, cwd string, settings contracts.Settings) *DirectHandler {
83+
registry := commands.Load(commands.Options{CWD: cwd, Settings: settings})
84+
return NewDirectHandler(DirectOptions{
85+
SessionID: sessionID,
86+
Manifest: BuildManifest(sessionID, cwd, registry),
87+
Registry: registry,
88+
})
89+
}
90+
91+
func (h *DirectHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
92+
switch normalizeDirectPath(r.URL.Path) {
93+
case "/health":
94+
h.handleHealth(w, r)
95+
case "/manifest":
96+
h.handleManifest(w, r)
97+
case "/resolve":
98+
h.handleResolve(w, r)
99+
case "/execute":
100+
h.handleExecute(w, r)
101+
default:
102+
writeDirectError(w, http.StatusNotFound, "bridge endpoint not found")
103+
}
104+
}
105+
106+
func (h *DirectHandler) handleHealth(w http.ResponseWriter, r *http.Request) {
107+
if r.Method != http.MethodGet {
108+
writeDirectError(w, http.StatusMethodNotAllowed, "method not allowed")
109+
return
110+
}
111+
writeDirectJSON(w, http.StatusOK, DirectHealthResponse{
112+
OK: true,
113+
SessionID: h.sessionID,
114+
Commands: len(h.manifest.Commands),
115+
})
116+
}
117+
118+
func (h *DirectHandler) handleManifest(w http.ResponseWriter, r *http.Request) {
119+
if r.Method != http.MethodGet {
120+
writeDirectError(w, http.StatusMethodNotAllowed, "method not allowed")
121+
return
122+
}
123+
writeDirectJSON(w, http.StatusOK, h.manifest)
124+
}
125+
126+
func (h *DirectHandler) handleResolve(w http.ResponseWriter, r *http.Request) {
127+
if r.Method != http.MethodPost {
128+
writeDirectError(w, http.StatusMethodNotAllowed, "method not allowed")
129+
return
130+
}
131+
req, ok := decodeDirectCommandRequest(w, r)
132+
if !ok {
133+
return
134+
}
135+
resolved := h.resolve(req.Command)
136+
writeDirectJSON(w, http.StatusOK, resolved)
137+
}
138+
139+
func (h *DirectHandler) handleExecute(w http.ResponseWriter, r *http.Request) {
140+
if r.Method != http.MethodPost {
141+
writeDirectError(w, http.StatusMethodNotAllowed, "method not allowed")
142+
return
143+
}
144+
req, ok := decodeDirectCommandRequest(w, r)
145+
if !ok {
146+
return
147+
}
148+
resolved := h.resolve(req.Command)
149+
if !resolved.Allowed || resolved.Command == nil {
150+
writeDirectJSON(w, http.StatusForbidden, DirectExecuteResponse{
151+
Allowed: false,
152+
Error: resolved.Reason,
153+
})
154+
return
155+
}
156+
canonical := canonicalSlashCommand(*resolved.Command, resolved.Args)
157+
result, handled, err := commands.ExecuteSlashCommand(h.registry, canonical, commands.SlashOptions{
158+
SessionID: h.sessionID,
159+
UUID: req.UUID,
160+
})
161+
response := DirectExecuteResponse{
162+
Allowed: true,
163+
Handled: handled,
164+
Command: resolved.Command,
165+
Name: resolved.Name,
166+
Args: resolved.Args,
167+
}
168+
if err != nil {
169+
response.Error = err.Error()
170+
writeDirectJSON(w, http.StatusInternalServerError, response)
171+
return
172+
}
173+
response.ShouldQuery = result.ShouldQuery
174+
response.Model = result.Model
175+
response.AllowedTools = append([]string(nil), result.AllowedTools...)
176+
response.Messages = len(result.Messages)
177+
response.Unknown = result.Unknown
178+
response.Unsupported = result.Unsupported
179+
response.ResultText = result.ResultText
180+
if result.LocalResult != nil {
181+
response.LocalResult = &DirectLocalResult{
182+
Type: result.LocalResult.Type,
183+
HasValue: strings.TrimSpace(result.LocalResult.Value) != "",
184+
}
185+
}
186+
writeDirectJSON(w, http.StatusOK, response)
187+
}
188+
189+
func (h *DirectHandler) resolve(raw string) DirectResolveResponse {
190+
raw = strings.TrimSpace(raw)
191+
if raw == "" {
192+
return DirectResolveResponse{Allowed: false, Reason: "command is required"}
193+
}
194+
command, ok := h.manifest.FindCommand(raw)
195+
if !ok {
196+
return DirectResolveResponse{Allowed: false, Reason: "command is not bridge-safe or is not registered"}
197+
}
198+
_, args := splitBridgeCommand(raw)
199+
return DirectResolveResponse{
200+
Allowed: true,
201+
Command: &command,
202+
Name: command.Name,
203+
Args: args,
204+
}
205+
}
206+
207+
func decodeDirectCommandRequest(w http.ResponseWriter, r *http.Request) (DirectCommandRequest, bool) {
208+
defer r.Body.Close()
209+
var req DirectCommandRequest
210+
decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 64*1024))
211+
decoder.DisallowUnknownFields()
212+
if err := decoder.Decode(&req); err != nil {
213+
writeDirectError(w, http.StatusBadRequest, "invalid JSON request: "+err.Error())
214+
return DirectCommandRequest{}, false
215+
}
216+
if strings.TrimSpace(req.Command) == "" {
217+
writeDirectError(w, http.StatusBadRequest, "command is required")
218+
return DirectCommandRequest{}, false
219+
}
220+
return req, true
221+
}
222+
223+
func canonicalSlashCommand(command Command, args string) string {
224+
if strings.TrimSpace(args) == "" {
225+
return "/" + command.Name
226+
}
227+
return "/" + command.Name + " " + strings.TrimSpace(args)
228+
}
229+
230+
func splitBridgeCommand(raw string) (string, string) {
231+
raw = strings.TrimSpace(strings.TrimPrefix(strings.TrimSpace(raw), "/"))
232+
if raw == "" {
233+
return "", ""
234+
}
235+
fields := strings.Fields(raw)
236+
if len(fields) == 0 {
237+
return "", ""
238+
}
239+
name := fields[0]
240+
offset := strings.Index(raw, name) + len(name)
241+
return name, strings.TrimSpace(raw[offset:])
242+
}
243+
244+
func normalizeDirectPath(path string) string {
245+
path = strings.TrimSpace(path)
246+
if path == "" {
247+
return "/"
248+
}
249+
path = strings.TrimRight(path, "/")
250+
if path == "" {
251+
path = "/"
252+
}
253+
if path == "/v1" {
254+
return "/"
255+
}
256+
if strings.HasPrefix(path, "/v1/") {
257+
path = strings.TrimPrefix(path, "/v1")
258+
}
259+
if path == "" {
260+
return "/"
261+
}
262+
if !strings.HasPrefix(path, "/") {
263+
return "/" + path
264+
}
265+
return path
266+
}
267+
268+
func writeDirectJSON(w http.ResponseWriter, status int, value any) {
269+
w.Header().Set("Content-Type", "application/json")
270+
w.WriteHeader(status)
271+
if err := json.NewEncoder(w).Encode(value); err != nil {
272+
_, _ = fmt.Fprintf(w, `{"error":%q}`+"\n", err.Error())
273+
}
274+
}
275+
276+
func writeDirectError(w http.ResponseWriter, status int, message string) {
277+
writeDirectJSON(w, status, DirectErrorResponse{Error: message})
278+
}

0 commit comments

Comments
 (0)