返回 DeepSeek-Reasonix
refs.go
1 package sessioninbox
2
3 import (
4 "context"
5 "crypto/sha256"
6 "encoding/hex"
7 "fmt"
8 "io"
9 "os"
10 "path/filepath"
11 "sort"
12 "strings"
13 )
14
15 const maxFrozenDirEntries = 100
16
17 // FreezeRefs snapshots explicit workspace paths without following symlinks.
18 // Controller-owned enqueue uses its typed resolver; this remains for old/API
19 // callers that provide an explicit path list.
20 func FreezeRefs(ctx context.Context, workspace string, paths []string) ([]RefSnapshot, error) {
21 _ = ctx
22 rootPath, err := filepath.Abs(strings.TrimSpace(workspace))
23 if err != nil || strings.TrimSpace(workspace) == "" {
24 return nil, fmt.Errorf("path requires workspace root")
25 }
26 root, err := os.OpenRoot(rootPath)
27 if err != nil {
28 return nil, fmt.Errorf("open workspace root: %w", err)
29 }
30 defer root.Close()
31
32 out := make([]RefSnapshot, 0, len(paths))
33 seen := map[string]struct{}{}
34 for _, path := range paths {
35 path = strings.TrimSpace(path)
36 if path == "" {
37 continue
38 }
39 if _, ok := seen[path]; ok {
40 continue
41 }
42 seen[path] = struct{}{}
43 snapshot, freezeErr := freezeScopedRef(root, rootPath, path)
44 if freezeErr != nil {
45 snapshot = RefSnapshot{
46 Kind: "frozen",
47 Path: path,
48 DisplayPath: path,
49 Content: fmt.Appendf(nil, "/* ref freeze failed: %v */", freezeErr),
50 }
51 }
52 out = append(out, snapshot)
53 }
54 return out, nil
55 }
56
57 func freezeScopedRef(root *os.Root, rootPath, path string) (RefSnapshot, error) {
58 rel, err := scopedRelativePath(rootPath, path)
59 if err != nil {
60 return RefSnapshot{}, err
61 }
62 info, err := root.Lstat(rel)
63 if err != nil {
64 return RefSnapshot{}, err
65 }
66 if info.Mode()&os.ModeSymlink != 0 {
67 return RefSnapshot{}, fmt.Errorf("path escapes workspace through symlink: %s", path)
68 }
69 if info.IsDir() {
70 content, err := freezeDirectory(root, rel)
71 if err != nil {
72 return RefSnapshot{}, err
73 }
74 return frozenSnapshot(rel, content, false), nil
75 }
76
77 file, err := root.Open(rel)
78 if err != nil {
79 return RefSnapshot{}, err
80 }
81 defer file.Close()
82 opened, err := file.Stat()
83 if err != nil {
84 return RefSnapshot{}, err
85 }
86 if !os.SameFile(info, opened) {
87 return RefSnapshot{}, fmt.Errorf("reference changed while opening: %s", path)
88 }
89 data, err := io.ReadAll(io.LimitReader(file, DefaultMaxItemBytes+1))
90 if err != nil {
91 return RefSnapshot{}, err
92 }
93 truncated := len(data) > DefaultMaxItemBytes
94 if truncated {
95 data = data[:DefaultMaxItemBytes]
96 }
97 after, err := file.Stat()
98 if err != nil || !os.SameFile(opened, after) || after.Size() != opened.Size() || !after.ModTime().Equal(opened.ModTime()) {
99 return RefSnapshot{}, fmt.Errorf("reference changed while reading: %s", path)
100 }
101 return frozenSnapshot(rel, data, truncated), nil
102 }
103
104 func scopedRelativePath(rootPath, path string) (string, error) {
105 candidate := filepath.Clean(path)
106 if filepath.IsAbs(candidate) {
107 rel, err := filepath.Rel(rootPath, candidate)
108 if err != nil || !filepath.IsLocal(rel) {
109 return "", fmt.Errorf("path outside workspace: %s", path)
110 }
111 candidate = rel
112 }
113 if !filepath.IsLocal(candidate) {
114 return "", fmt.Errorf("path outside workspace: %s", path)
115 }
116 return candidate, nil
117 }
118
119 func freezeDirectory(root *os.Root, rel string) ([]byte, error) {
120 var entries []string
121 if err := walkFrozenDirectory(root, rel, rel, &entries); err != nil {
122 return nil, err
123 }
124 sort.Strings(entries)
125 if len(entries) > maxFrozenDirEntries {
126 entries = append(entries[:maxFrozenDirEntries], "…[truncated; directory has more entries]…")
127 }
128 return []byte(strings.Join(entries, "\n")), nil
129 }
130
131 func walkFrozenDirectory(root *os.Root, dir, base string, entries *[]string) error {
132 if len(*entries) > maxFrozenDirEntries {
133 return nil
134 }
135 before, err := root.Lstat(dir)
136 if err != nil {
137 return err
138 }
139 if before.Mode()&os.ModeSymlink != 0 || !before.IsDir() {
140 return fmt.Errorf("directory reference changed while opening: %s", dir)
141 }
142 opened, err := root.Open(dir)
143 if err != nil {
144 return err
145 }
146 after, err := opened.Stat()
147 if err != nil || !os.SameFile(before, after) {
148 opened.Close()
149 return fmt.Errorf("directory reference changed while opening: %s", dir)
150 }
151 children, err := opened.ReadDir(-1)
152 opened.Close()
153 if err != nil {
154 return err
155 }
156 for _, child := range children {
157 path := filepath.Join(dir, child.Name())
158 info, err := root.Lstat(path)
159 if err != nil {
160 return err
161 }
162 if info.Mode()&os.ModeSymlink != 0 || skipFrozenDirEntry(child.Name(), info.IsDir()) {
163 continue
164 }
165 display, relErr := filepath.Rel(base, path)
166 if relErr != nil || !filepath.IsLocal(display) {
167 return fmt.Errorf("directory entry escapes reference root")
168 }
169 display = filepath.ToSlash(display)
170 if info.IsDir() {
171 *entries = append(*entries, display+"/")
172 if err := walkFrozenDirectory(root, path, base, entries); err != nil {
173 return err
174 }
175 } else {
176 *entries = append(*entries, display)
177 }
178 if len(*entries) > maxFrozenDirEntries {
179 return nil
180 }
181 }
182 return nil
183 }
184
185 func skipFrozenDirEntry(name string, isDir bool) bool {
186 if name == ".DS_Store" || name == "Thumbs.db" {
187 return true
188 }
189 if !isDir {
190 return false
191 }
192 switch name {
193 case ".git", ".idea", ".vscode", "build", "dist", "node_modules", "__pycache__":
194 return true
195 default:
196 return false
197 }
198 }
199
200 func frozenSnapshot(path string, content []byte, truncated bool) RefSnapshot {
201 sum := sha256.Sum256(content)
202 path = filepath.ToSlash(path)
203 return RefSnapshot{
204 Kind: "frozen",
205 Path: path,
206 DisplayPath: path,
207 Content: content,
208 ContentSHA: hex.EncodeToString(sum[:]),
209 Truncated: truncated,
210 }
211 }
212
213 // ApplyFrozenRefs keeps legacy version-1 blobs readable. New entries persist
214 // the exact typed reference block in PromptEnvelope.FrozenRefBlock.
215 func ApplyFrozenRefs(submit string, bodies map[string]string) string {
216 if len(bodies) == 0 {
217 return submit
218 }
219 paths := make([]string, 0, len(bodies))
220 for path := range bodies {
221 paths = append(paths, path)
222 }
223 sort.Strings(paths)
224 var b strings.Builder
225 b.WriteString(submit)
226 b.WriteString("\n\n<!-- frozen inbox references (enqueue-time snapshot) -->\n")
227 for _, path := range paths {
228 fmt.Fprintf(&b, "\n### @%s\n```\n%s\n```\n", path, bodies[path])
229 }
230 return b.String()
231 }
232
233 // MaterializeRefs validates stored legacy snapshots without reading live paths.
234 // Clean-git entries from pre-fix blobs are paused until the user refreshes them.
235 func MaterializeRefs(_ context.Context, _ string, refs []RefSnapshot) (string, map[string]string, error) {
236 bodies := make(map[string]string, len(refs))
237 for _, ref := range refs {
238 if ref.Kind == "clean_git" {
239 return fmt.Sprintf("legacy clean-git reference %s requires refresh", ref.Path), bodies, nil
240 }
241 if ref.ContentSHA != "" {
242 sum := sha256.Sum256(ref.Content)
243 if hex.EncodeToString(sum[:]) != ref.ContentSHA {
244 return fmt.Sprintf("checksum mismatch for frozen ref %s", ref.Path), bodies, nil
245 }
246 }
247 bodies[firstNonEmpty(ref.DisplayPath, ref.Path)] = string(ref.Content)
248 }
249 return "", bodies, nil
250 }
251
251 lines GO