package handlers import ( "context" "encoding/json" "errors" "io/fs" "os" "strings" "github.com/yan1h/agent-coding-workflow/internal/acp/pathsafe" ) // FsReadHandler handles fs/read_text_file (spec §5.4). type FsReadHandler struct{} // NewFsReadHandler constructs an FsReadHandler. func NewFsReadHandler() *FsReadHandler { return &FsReadHandler{} } // fsReadParams is the ACP-protocol input for fs/read_text_file. type fsReadParams struct { SessionID string `json:"sessionId"` Path string `json:"path"` Line *int `json:"line,omitempty"` Limit *int `json:"limit,omitempty"` } type fsReadResult struct { Content string `json:"content"` } // Handle reads a file inside sess.CwdPath, returning content (with optional // line/limit slicing). Returns FsError for path-escape or read failure. func (h *FsReadHandler) Handle(_ context.Context, sess SessionContext, paramsJSON json.RawMessage) FsResult { var p fsReadParams if err := json.Unmarshal(paramsJSON, &p); err != nil { return FsResult{Err: &FsError{Code: -32602, Message: "invalid params: " + err.Error()}} } abs, err := resolveSafePath(sess.CwdPath, p.Path) if err != nil { return FsResult{Err: &FsError{Code: -32001, Message: err.Error()}} } data, err := os.ReadFile(abs) if err != nil { if errors.Is(err, fs.ErrNotExist) { return FsResult{Err: &FsError{Code: -32603, Message: "file not found"}} } return FsResult{Err: &FsError{Code: -32603, Message: "read: " + err.Error()}} } content := string(data) if p.Line != nil || p.Limit != nil { content = sliceLines(content, p.Line, p.Limit) } out, _ := json.Marshal(fsReadResult{Content: content}) return FsResult{OK: out} } // sliceLines returns lines[line:line+limit] joined by "\n". // line is 0-based; limit caps the line count. func sliceLines(s string, line, limit *int) string { lines := strings.Split(s, "\n") start := 0 if line != nil && *line > 0 { start = *line } if start > len(lines) { start = len(lines) } end := len(lines) if limit != nil && *limit >= 0 { // overflow-safe:用剩余行数为上限做钳制,避免 start+*limit 在 *limit 很大 // (如 math.MaxInt)时整数溢出为负,绕过下界判断后导致 lines[start:end] panic。 remaining := len(lines) - start if *limit < remaining { end = start + *limit } } return strings.Join(lines[start:end], "\n") } // resolveSafePath 委托共享实现 pathsafe.ResolveSafePath。 // handlers 子包不能 import internal/acp 主包(acp 依赖 handlers,会循环), // 故校验逻辑抽到 internal/acp/pathsafe 子包共用。 func resolveSafePath(cwd, requested string) (string, error) { return pathsafe.ResolveSafePath(cwd, requested) }