@@ -28,20 +28,22 @@ const (
2828// - Leading-dot filenames are rejected because they are hidden by convention;
2929// the sentinel is written by the runner itself and is never staged by clients.
3030func validateKnowledgeRelPath (relPath string ) error {
31+ if relPath == "" {
32+ return fmt .Errorf ("rel_path must not be empty" )
33+ }
3134 if strings .ContainsAny (relPath , `/\` ) {
3235 return fmt .Errorf ("rel_path must not contain path separators: %q" , relPath )
3336 }
34- if relPath == ".." || strings .Contains (relPath , ".." ) {
35- return fmt .Errorf ("rel_path must not contain '..': %q" , relPath )
37+ // Reject the bare ".." token. Slash-separated traversal like "foo/../bar"
38+ // is already blocked above, but a plain ".." with no slashes still escapes.
39+ if relPath == ".." {
40+ return fmt .Errorf ("rel_path must not be '..': %q" , relPath )
3641 }
3742 if strings .HasPrefix (relPath , "." ) {
3843 // Hidden files (including the sentinel itself) cannot be staged by clients.
3944 // The runner owns the sentinel exclusively.
4045 return fmt .Errorf ("rel_path must not start with '.': %q" , relPath )
4146 }
42- if relPath == "" {
43- return fmt .Errorf ("rel_path must not be empty" )
44- }
4547 return nil
4648}
4749
@@ -210,33 +212,27 @@ func (w *Workspace) StageKnowledgeFiles(ctx context.Context, args *protocol.Stag
210212// DeleteKnowledgeFiles removes the supplied files from the workspace root and
211213// scrubs their entries from the sentinel.
212214func (w * Workspace ) DeleteKnowledgeFiles (ctx context.Context , args * protocol.DeleteKnowledgeFilesArgs ) (* protocol.DeleteKnowledgeFilesResult , error ) {
213- var removed []string
215+ // toRemove collects validated rel_paths so we can clean the sentinel in one
216+ // locked pass after the file removals.
217+ toRemove := make (map [string ]struct {}, len (args .RelPaths ))
214218
215219 for _ , relPath := range args .RelPaths {
216220 if err := validateKnowledgeRelPath (relPath ); err != nil {
217221 slog .Warn ("skipping invalid rel_path in delete_knowledge_files" , "rel_path" , relPath , "error" , err )
218222 continue
219223 }
224+ toRemove [relPath ] = struct {}{}
220225
221226 targetPath := filepath .Join (w .root , relPath )
222227 if err := os .Remove (targetPath ); err != nil && ! os .IsNotExist (err ) {
223228 slog .Warn ("failed to remove knowledge file" , "rel_path" , relPath , "error" , err )
224- continue
225- }
226- removed = append (removed , relPath )
227- }
228-
229- // Remove entries from sentinel for files we attempted to delete (whether
230- // they existed or not — idempotent means we clean the sentinel too).
231- sentinelPath := filepath .Join (w .root , sentinelName )
232- toRemove := make (map [string ]struct {}, len (args .RelPaths ))
233- for _ , rp := range args .RelPaths {
234- if validateKnowledgeRelPath (rp ) == nil {
235- toRemove [rp ] = struct {}{}
236229 }
237230 }
238231
232+ // Remove entries from sentinel for all valid paths (idempotent — missing
233+ // sentinel entries are simply no-ops in the delete loop).
239234 if len (toRemove ) > 0 {
235+ sentinelPath := filepath .Join (w .root , sentinelName )
240236 if err := withSentinelLock (sentinelPath , func () error {
241237 m := readSentinel (sentinelPath )
242238 for rp := range toRemove {
@@ -248,6 +244,5 @@ func (w *Workspace) DeleteKnowledgeFiles(ctx context.Context, args *protocol.Del
248244 }
249245 }
250246
251- _ = removed // logged individually above; all valid paths are processed
252247 return & protocol.DeleteKnowledgeFilesResult {Success : true }, nil
253248}
0 commit comments