Skip to content

Commit 84ea19d

Browse files
ysyneuclaude
andcommitted
refactor(knowledge): fix dead variable, double validation, and over-strict '..' check
- Remove the unused `removed []string` accumulator in DeleteKnowledgeFiles; valid paths now go straight into `toRemove` in one loop pass, eliminating the second validateKnowledgeRelPath call per path. - Tighten the '..' guard to reject only the bare ".." token — the old strings.Contains check wrongly rejected valid filenames like "foo..bar". - Add test assertions confirming double-dot-in-middle filenames are accepted. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 5d64095 commit 84ea19d

2 files changed

Lines changed: 20 additions & 19 deletions

File tree

workspace/knowledge.go

Lines changed: 14 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -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.
3030
func 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.
212214
func (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
}

workspace/knowledge_test.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,12 @@ func TestValidateKnowledgeRelPath(t *testing.T) {
225225

226226
invalid := []string{
227227
"", "..", "a/b", `a\b`, ".hidden", sentinelName, "foo/../bar",
228+
// These are now valid because ".." only matches the bare token, not
229+
// substrings — "foo..bar" is a legitimate flat filename.
230+
}
231+
validButPreviouslyRejected := []string{"foo..bar", "v2..md"}
232+
for _, p := range validButPreviouslyRejected {
233+
assert.NoError(t, validateKnowledgeRelPath(p), "should be valid (double-dot in middle): %q", p)
228234
}
229235
for _, p := range invalid {
230236
assert.Error(t, validateKnowledgeRelPath(p), "should be invalid: %q", p)

0 commit comments

Comments
 (0)