返回 DeepSeek-Reasonix
upload.go
根目录 / internal / browser / cdp / upload.go
1 package cdp
2
3 import (
4 "fmt"
5 "os"
6 "path/filepath"
7 "strings"
8 )
9
10 // uploadRoots decides which files browser_upload may hand to a page. A page is
11 // untrusted: an unrestricted file input would let any site the agent visits
12 // receive any file this process can read, so a path outside the session's own
13 // roots is refused. The tool promises the model that only files the task owns
14 // can be attached, and this is where that promise is kept.
15 type uploadRoots struct {
16 roots []string
17 }
18
19 // newUploadRoots takes the session's roots plus the executor's artifact
20 // directory, so a file the agent just downloaded can be attached too. Each
21 // root is also recorded in its symlink-resolved form, because a platform that
22 // hands out /tmp for /private/tmp would otherwise refuse the same file
23 // depending on which spelling the model used.
24 func newUploadRoots(configured []string, artifacts string) uploadRoots {
25 roots := make([]string, 0, 2*(len(configured)+1))
26 for _, root := range append(append([]string{}, configured...), artifacts) {
27 root = strings.TrimSpace(root)
28 if root == "" {
29 continue
30 }
31 abs, err := filepath.Abs(root)
32 if err != nil {
33 continue
34 }
35 abs = filepath.Clean(abs)
36 roots = append(roots, abs)
37 if real, err := filepath.EvalSymlinks(abs); err == nil && real != abs {
38 roots = append(roots, real)
39 }
40 }
41 return uploadRoots{roots: roots}
42 }
43
44 // resolve returns the path of an upload candidate, or the reason the model
45 // cannot attach it. Containment is enforced by os.Root rather than by
46 // comparing cleaned strings: a root refuses both traversal and a symlink
47 // leaving it, so a link the agent can write inside the workspace cannot point
48 // a file input at a private key.
49 func (u uploadRoots) resolve(path string) (string, string) {
50 abs, err := filepath.Abs(path)
51 if err != nil {
52 return "", fmt.Sprintf("file %s: %v", path, err)
53 }
54 for _, root := range u.roots {
55 rel, err := filepath.Rel(root, abs)
56 if err != nil || rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
57 continue
58 }
59 confined, err := os.OpenRoot(root)
60 if err != nil {
61 continue
62 }
63 reason := statRegularFile(confined, rel, path)
64 confined.Close()
65 if reason != "" {
66 return "", reason
67 }
68 return abs, ""
69 }
70 return "", fmt.Sprintf("file %s is outside this task's directories, so it cannot be attached to a page", path)
71 }
72
73 // statRegularFile reports why rel cannot be uploaded from confined, or "" when
74 // it is a readable regular file inside it.
75 func statRegularFile(confined *os.Root, rel, display string) string {
76 info, err := confined.Stat(rel)
77 if err != nil {
78 return fmt.Sprintf("file %s is not readable: %v", display, err)
79 }
80 if !info.Mode().IsRegular() {
81 return fmt.Sprintf("%s is not a regular file", display)
82 }
83 return ""
84 }
85
85 lines GO