mirror of
https://github.com/RuoJi6/ARTEX.git
synced 2026-10-28 09:13:49 +08:00
feat: expose scripts to review and add approval side chat
This commit is contained in:
1 parent
ee413324c8
commit
9b0d1bdb3c
18 files changed
+935
-217
No files matched your search
@@ -0,0 +1,339 @@
|
||||
package intercept
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"mvdan.cc/sh/v3/syntax"
|
||||
)
|
||||
|
||||
var scriptInterpreter = regexp.MustCompile(`^(python[0-9.]*|bash|sh|zsh|fish|node|nodejs|ruby|perl|php|pwsh|powershell)$`)
|
||||
|
||||
type scriptReference struct {
|
||||
path, problem string
|
||||
direct bool
|
||||
}
|
||||
|
||||
// Only statically resolvable shell words are used. No environment expansion,
|
||||
// command substitution, interpreter or shell is executed to discover a path.
|
||||
func staticWord(w *syntax.Word) (string, bool) {
|
||||
if w == nil {
|
||||
return "", false
|
||||
}
|
||||
return staticParts(w.Parts)
|
||||
}
|
||||
func staticParts(parts []syntax.WordPart) (string, bool) {
|
||||
var b strings.Builder
|
||||
for _, part := range parts {
|
||||
switch p := part.(type) {
|
||||
case *syntax.Lit:
|
||||
if strings.ContainsAny(p.Value, "*?[]~\\") {
|
||||
return "", false
|
||||
}
|
||||
b.WriteString(p.Value)
|
||||
case *syntax.SglQuoted:
|
||||
if p.Dollar {
|
||||
return "", false
|
||||
}
|
||||
b.WriteString(p.Value)
|
||||
case *syntax.DblQuoted:
|
||||
s, ok := staticParts(p.Parts)
|
||||
if !ok {
|
||||
return "", false
|
||||
}
|
||||
b.WriteString(s)
|
||||
default:
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
return b.String(), true
|
||||
}
|
||||
|
||||
func scriptReferences(command, dir string, depth int) []scriptReference {
|
||||
if depth > 4 {
|
||||
return []scriptReference{{problem: "嵌套 Shell 命令超过静态解析范围"}}
|
||||
}
|
||||
tree, err := syntax.NewParser().Parse(strings.NewReader(command), "review")
|
||||
if err != nil {
|
||||
return []scriptReference{{problem: "无法静态解析当前 Shell 命令"}}
|
||||
}
|
||||
var refs []scriptReference
|
||||
add := func(word *syntax.Word, direct bool) {
|
||||
path, ok := staticWord(word)
|
||||
if !ok || path == "" {
|
||||
refs = append(refs, scriptReference{problem: "执行脚本路径包含动态展开,无法确定实际文件"})
|
||||
return
|
||||
}
|
||||
if !filepath.IsAbs(path) {
|
||||
if dir == "" {
|
||||
refs = append(refs, scriptReference{path: path, problem: "执行目录不确定,无法解析相对脚本路径"})
|
||||
return
|
||||
}
|
||||
path = filepath.Join(dir, path)
|
||||
}
|
||||
absolute, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
refs = append(refs, scriptReference{path: path, problem: "无法解析绝对路径"})
|
||||
return
|
||||
}
|
||||
refs = append(refs, scriptReference{path: absolute, direct: direct})
|
||||
}
|
||||
// Walking the AST visits calls inside command substitutions as well, but not
|
||||
// string literals, comments or heredoc bodies. Function bodies/conditional cd
|
||||
// cannot establish a reliable working directory for subsequent calls.
|
||||
syntax.Walk(tree, func(node syntax.Node) bool {
|
||||
if _, ok := node.(*syntax.FuncDecl); ok {
|
||||
return false
|
||||
}
|
||||
stmt, ok := node.(*syntax.Stmt)
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
call, ok := stmt.Cmd.(*syntax.CallExpr)
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
args := call.Args
|
||||
if len(args) == 0 {
|
||||
return true
|
||||
}
|
||||
name, ok := staticWord(args[0])
|
||||
if !ok {
|
||||
refs = append(refs, scriptReference{problem: "动态命令入口,无法确定是否执行脚本"})
|
||||
return true
|
||||
}
|
||||
// Common transparent wrappers. Unknown wrapper flags are not evaluated.
|
||||
for len(args) > 1 && (name == "env" || name == "sudo" || name == "exec" || name == "timeout" || name == "nohup") {
|
||||
args = args[1:]
|
||||
if name == "timeout" && len(args) > 1 {
|
||||
args = args[1:]
|
||||
}
|
||||
for len(args) > 1 {
|
||||
v, literal := staticWord(args[0])
|
||||
if !literal || (!strings.HasPrefix(v, "-") && !(name == "env" && strings.Contains(v, "="))) {
|
||||
break
|
||||
}
|
||||
args = args[1:]
|
||||
}
|
||||
name, ok = staticWord(args[0])
|
||||
if !ok {
|
||||
refs = append(refs, scriptReference{problem: "包装命令的执行入口不确定"})
|
||||
return true
|
||||
}
|
||||
}
|
||||
base := filepath.Base(name)
|
||||
if base == "cd" {
|
||||
if len(args) == 2 {
|
||||
path, known := staticWord(args[1])
|
||||
if known && path != "-" {
|
||||
if filepath.IsAbs(path) {
|
||||
dir = path
|
||||
} else if dir != "" {
|
||||
dir = filepath.Join(dir, path)
|
||||
}
|
||||
} else {
|
||||
dir = ""
|
||||
}
|
||||
} else {
|
||||
dir = ""
|
||||
}
|
||||
return true
|
||||
}
|
||||
if base == "source" || base == "." {
|
||||
if len(args) > 1 {
|
||||
add(args[1], false)
|
||||
} else {
|
||||
refs = append(refs, scriptReference{problem: "source 缺少脚本路径"})
|
||||
}
|
||||
return true
|
||||
}
|
||||
if scriptInterpreter.MatchString(base) {
|
||||
shell := base == "bash" || base == "sh" || base == "zsh" || base == "fish"
|
||||
for i := 1; i < len(args); i++ {
|
||||
value, literal := staticWord(args[i])
|
||||
if !literal {
|
||||
add(args[i], false)
|
||||
return true
|
||||
}
|
||||
if value == "-c" || value == "-e" || value == "--eval" || value == "--print" || (shell && strings.HasPrefix(value, "-") && strings.Contains(value, "c")) {
|
||||
if shell && i+1 < len(args) {
|
||||
inline, known := staticWord(args[i+1])
|
||||
if known {
|
||||
refs = append(refs, scriptReferences(inline, dir, depth+1)...)
|
||||
} else {
|
||||
refs = append(refs, scriptReference{problem: "Shell 内联执行内容不确定"})
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
if value == "-m" {
|
||||
refs = append(refs, scriptReference{problem: "模块执行未提供可确定的本地脚本文件"})
|
||||
return true
|
||||
}
|
||||
if value == "-" {
|
||||
break
|
||||
}
|
||||
if value == "--" {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(value, "-") {
|
||||
if value == "-W" || value == "-X" || value == "--require" || value == "-r" {
|
||||
i++
|
||||
}
|
||||
continue
|
||||
}
|
||||
add(args[i], false)
|
||||
return true
|
||||
}
|
||||
for _, r := range stmt.Redirs {
|
||||
if r.Op == syntax.RdrIn {
|
||||
add(r.Word, false)
|
||||
return true
|
||||
}
|
||||
if r.Hdoc != nil || r.Op == syntax.WordHdoc {
|
||||
return true
|
||||
}
|
||||
}
|
||||
if len(args) > 1 || len(tree.Stmts) > 1 || strings.Contains(command, "|") {
|
||||
refs = append(refs, scriptReference{problem: "解释器从外部标准输入执行代码,无法取得文件快照"})
|
||||
}
|
||||
return true
|
||||
}
|
||||
if strings.Contains(name, "/") || knownScriptExtension(name) {
|
||||
add(args[0], true)
|
||||
}
|
||||
return true
|
||||
})
|
||||
return refs
|
||||
}
|
||||
func knownScriptExtension(path string) bool {
|
||||
switch strings.ToLower(filepath.Ext(path)) {
|
||||
case ".sh", ".bash", ".zsh", ".fish", ".py", ".py3", ".js", ".mjs", ".cjs", ".ts", ".tsx", ".rb", ".pl", ".php", ".ps1", ".lua":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func captureScripts(workingDir, tool string, arguments json.RawMessage) ([]ReviewScript, string) {
|
||||
switch tool {
|
||||
case "Bash", "shell_open", "shell_send", "command", "script":
|
||||
default:
|
||||
return nil, "not_applicable"
|
||||
}
|
||||
var fields map[string]any
|
||||
if json.Unmarshal(arguments, &fields) != nil {
|
||||
return nil, "not_applicable"
|
||||
}
|
||||
key := "command"
|
||||
if tool == "shell_send" {
|
||||
key = "text"
|
||||
}
|
||||
command, _ := fields[key].(string)
|
||||
if strings.TrimSpace(command) == "" {
|
||||
for _, candidate := range []string{"script", "code", "cmd"} {
|
||||
if value, ok := fields[candidate].(string); ok {
|
||||
command = value
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if strings.TrimSpace(command) == "" {
|
||||
return nil, "not_applicable"
|
||||
}
|
||||
refs := scriptReferences(command, workingDir, 0)
|
||||
var files []ReviewScript
|
||||
seen := map[string]bool{}
|
||||
status := "complete"
|
||||
total := 0
|
||||
for _, ref := range refs {
|
||||
if seen[ref.path+ref.problem] {
|
||||
continue
|
||||
}
|
||||
seen[ref.path+ref.problem] = true
|
||||
file := ReviewScript{Path: ref.path, ReadError: ref.problem}
|
||||
if file.Path == "" {
|
||||
file.Path = "<无法确定的脚本>"
|
||||
}
|
||||
if ref.problem != "" {
|
||||
status = "incomplete"
|
||||
files = append(files, file)
|
||||
continue
|
||||
}
|
||||
if len(files) >= 8 {
|
||||
files = append(files, ReviewScript{Path: file.Path, ReadError: "本次调用超过 8 个脚本文件"})
|
||||
status = "incomplete"
|
||||
break
|
||||
}
|
||||
content, err := readScript(ref.path)
|
||||
if err != nil {
|
||||
file.ReadError = err.Error()
|
||||
if strings.Contains(file.ReadError, "超过") {
|
||||
file.Truncated = true
|
||||
}
|
||||
status = "incomplete"
|
||||
files = append(files, file)
|
||||
continue
|
||||
}
|
||||
file.Bytes = int64(len(content))
|
||||
// A directly invoked native binary is not a script. Interpreters, however,
|
||||
// must never be permitted to execute an opaque/non-text file automatically.
|
||||
if ref.direct && (bytes.HasPrefix(content, []byte{0x7f, 'E', 'L', 'F'}) || bytes.HasPrefix(content, []byte{'M', 'Z'})) {
|
||||
continue
|
||||
}
|
||||
if tool == "shell_send" {
|
||||
file.ReadError = "交互会话可能已切换目录或连接远端,无法确认本机文件就是实际执行脚本"
|
||||
status = "incomplete"
|
||||
files = append(files, file)
|
||||
continue
|
||||
}
|
||||
if !utf8.Valid(content) || bytes.ContainsRune(content, 0) {
|
||||
file.ReadError = "脚本不是有效 UTF-8 文本"
|
||||
status = "incomplete"
|
||||
files = append(files, file)
|
||||
continue
|
||||
}
|
||||
if len(content) > maxReviewScriptBytes || total+len(content) > maxReviewScriptBytes {
|
||||
file.ReadError = "脚本超过完整快照总量限制(1 MiB),未附加部分内容"
|
||||
file.Truncated = true
|
||||
status = "incomplete"
|
||||
files = append(files, file)
|
||||
continue
|
||||
}
|
||||
hash := sha256.Sum256(content)
|
||||
file.SHA256 = fmt.Sprintf("%x", hash)
|
||||
file.Content = string(content)
|
||||
file.Complete = true
|
||||
total += len(content)
|
||||
files = append(files, file)
|
||||
}
|
||||
if len(files) == 0 {
|
||||
return nil, "not_applicable"
|
||||
}
|
||||
return files, status
|
||||
}
|
||||
func readScript(path string) ([]byte, error) {
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !info.Mode().IsRegular() {
|
||||
return nil, fmt.Errorf("不是普通文件")
|
||||
}
|
||||
if info.Size() > maxReviewScriptBytes {
|
||||
return nil, fmt.Errorf("脚本超过完整快照限制(1 MiB)")
|
||||
}
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
return io.ReadAll(io.LimitReader(f, maxReviewScriptBytes+1))
|
||||
}
|
||||
Reference in new issue
Block a user