@@ -23,58 +23,6 @@ func (h *handlers) ragIndexRoot() string {
2323 return dir
2424}
2525
26- // @intent normalize a docs/index root to an absolute, symlink-evaluated path before path checks.
27- // @requires root must be a filesystem path that can be resolved or created as needed.
28- // @ensures returned path is absolute, cleaned, and symlink-resolved when it exists.
29- // @domainRule safe-root containment checks must happen after symlink evaluation.
30- // @sideEffect may create the root directory on disk when create is true.
31- func resolveSafeRoot (root string , create bool ) (string , error ) {
32- absRoot , err := filepath .Abs (root )
33- if err != nil {
34- return "" , fmt .Errorf ("resolve safe root: %w" , err )
35- }
36- if create {
37- if err := os .MkdirAll (absRoot , 0o755 ); err != nil {
38- return "" , fmt .Errorf ("create safe root: %w" , err )
39- }
40- }
41- if _ , err := os .Stat (absRoot ); err == nil {
42- realRoot , err := filepath .EvalSymlinks (absRoot )
43- if err != nil {
44- return "" , fmt .Errorf ("resolve safe root symlinks: %w" , err )
45- }
46- return filepath .Clean (realRoot ), nil
47- } else if ! os .IsNotExist (err ) {
48- return "" , fmt .Errorf ("stat safe root: %w" , err )
49- }
50- return filepath .Clean (absRoot ), nil
51- }
52-
53- // @intent reject relative paths that would resolve outside the resolved docs root.
54- // @requires relPath must be a relative, traversal-free path fragment.
55- // @ensures returned path stays within the resolved safe root and has no symlink escape.
56- // @domainRule traversal checks happen before symlink evaluation, and containment checks happen after it.
57- // @sideEffect may create the configured root directory indirectly through resolveSafeRoot when createRoot is true.
58- func safePathUnderRoot (root , relPath , field string , createRoot bool , allowMissingLeaf bool ) (string , error ) {
59- clean := filepath .Clean (relPath )
60- if filepath .IsAbs (clean ) || strings .HasPrefix (clean , ".." ) {
61- return "" , fmt .Errorf ("invalid %s: path traversal not allowed" , field )
62- }
63- base , err := resolveSafeRoot (root , createRoot )
64- if err != nil {
65- return "" , err
66- }
67- target , err := ensureNoSymlinkInPath (base , clean , allowMissingLeaf )
68- if err != nil {
69- return "" , fmt .Errorf ("resolve %s: %w" , field , err )
70- }
71- target = filepath .Clean (target )
72- if target != base && ! strings .HasPrefix (target , base + string (os .PathSeparator )) {
73- return "" , fmt .Errorf ("%s %q is outside configured safe root" , field , relPath )
74- }
75- return target , nil
76- }
77-
7826// getDocContent reads a generated documentation file by relative path.
7927// @intent Returns the content of a documentation file directly so agents can read detailed descriptions.
8028// @param request file_path is the relative documentation path based on the working directory.
@@ -200,3 +148,55 @@ func (h *handlers) searchDocsFromDB(ctx context.Context, namespace, query string
200148 }
201149 return results , nil
202150}
151+
152+ // @intent normalize a docs/index root to an absolute, symlink-evaluated path before path checks.
153+ // @requires root must be a filesystem path that can be resolved or created as needed.
154+ // @ensures returned path is absolute, cleaned, and symlink-resolved when it exists.
155+ // @domainRule safe-root containment checks must happen after symlink evaluation.
156+ // @sideEffect may create the root directory on disk when create is true.
157+ func resolveSafeRoot (root string , create bool ) (string , error ) {
158+ absRoot , err := filepath .Abs (root )
159+ if err != nil {
160+ return "" , fmt .Errorf ("resolve safe root: %w" , err )
161+ }
162+ if create {
163+ if err := os .MkdirAll (absRoot , 0o755 ); err != nil {
164+ return "" , fmt .Errorf ("create safe root: %w" , err )
165+ }
166+ }
167+ if _ , err := os .Stat (absRoot ); err == nil {
168+ realRoot , err := filepath .EvalSymlinks (absRoot )
169+ if err != nil {
170+ return "" , fmt .Errorf ("resolve safe root symlinks: %w" , err )
171+ }
172+ return filepath .Clean (realRoot ), nil
173+ } else if ! os .IsNotExist (err ) {
174+ return "" , fmt .Errorf ("stat safe root: %w" , err )
175+ }
176+ return filepath .Clean (absRoot ), nil
177+ }
178+
179+ // @intent reject relative paths that would resolve outside the resolved docs root.
180+ // @requires relPath must be a relative, traversal-free path fragment.
181+ // @ensures returned path stays within the resolved safe root and has no symlink escape.
182+ // @domainRule traversal checks happen before symlink evaluation, and containment checks happen after it.
183+ // @sideEffect may create the configured root directory indirectly through resolveSafeRoot when createRoot is true.
184+ func safePathUnderRoot (root , relPath , field string , createRoot bool , allowMissingLeaf bool ) (string , error ) {
185+ clean := filepath .Clean (relPath )
186+ if filepath .IsAbs (clean ) || strings .HasPrefix (clean , ".." ) {
187+ return "" , fmt .Errorf ("invalid %s: path traversal not allowed" , field )
188+ }
189+ base , err := resolveSafeRoot (root , createRoot )
190+ if err != nil {
191+ return "" , err
192+ }
193+ target , err := ensureNoSymlinkInPath (base , clean , allowMissingLeaf )
194+ if err != nil {
195+ return "" , fmt .Errorf ("resolve %s: %w" , field , err )
196+ }
197+ target = filepath .Clean (target )
198+ if target != base && ! strings .HasPrefix (target , base + string (os .PathSeparator )) {
199+ return "" , fmt .Errorf ("%s %q is outside configured safe root" , field , relPath )
200+ }
201+ return target , nil
202+ }
0 commit comments