fix(agentctl): size file-search results from trusted bounds (#2434)
Co-authored-by: Kandev Agent <agent@kandev.dev> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -893,13 +893,13 @@ type fileSearchCandidate struct {
|
||||
const (
|
||||
// fileSearchDefaultLimit applies when the caller passes a non-positive limit.
|
||||
fileSearchDefaultLimit = 20
|
||||
// fileSearchMaxLimit caps the caller-supplied limit before it is used to
|
||||
// pre-allocate the result slice. The limit reaches this code straight from
|
||||
// the `limit` query parameter of GET /api/v1/workspace/search, so without a
|
||||
// ceiling a request like `?limit=2000000000` makes the process reserve
|
||||
// gigabytes for a result set that can never exceed the number of tracked
|
||||
// files. Mirrors the clamp validateContentSearch already applies to
|
||||
// content search.
|
||||
// fileSearchMaxLimit caps how many results one search may return. The limit
|
||||
// reaches this code straight from the `limit` query parameter of
|
||||
// GET /api/v1/workspace/search, so without a ceiling a request like
|
||||
// `?limit=2000000000` would ask for a result set that can never exceed the
|
||||
// number of tracked files. It also bounds the result slice's capacity,
|
||||
// which is sized from this cap rather than from the request. Mirrors the
|
||||
// clamp validateContentSearch already applies to content search.
|
||||
fileSearchMaxLimit = 200
|
||||
)
|
||||
|
||||
@@ -1100,8 +1100,11 @@ func searchFileCandidates(
|
||||
return len(matches[i].matchPath) < len(matches[j].matchPath)
|
||||
})
|
||||
|
||||
// Return top limit results
|
||||
result := make([]types.FileSearchResult, 0, limit)
|
||||
// Return top limit results. The capacity is derived from values we own —
|
||||
// the matches we actually collected and the hard cap — instead of the
|
||||
// caller-supplied limit, so the allocation size never depends on request
|
||||
// input even indirectly.
|
||||
result := make([]types.FileSearchResult, 0, min(len(matches), fileSearchMaxLimit))
|
||||
for i := 0; i < len(matches) && i < limit; i++ {
|
||||
result = append(result, types.FileSearchResult{
|
||||
RepositoryName: matches[i].repositoryName,
|
||||
|
||||
@@ -541,6 +541,27 @@ func TestSearchFileCandidatesClampsCallerSuppliedLimit(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// The result slice must be sized from the matches we collected, never from the
|
||||
// caller-supplied limit, so a workspace with a handful of files cannot be made
|
||||
// to reserve a large slice by a single request.
|
||||
func TestSearchFileCandidatesSizesResultFromMatchCount(t *testing.T) {
|
||||
candidates := []fileSearchCandidate{
|
||||
{path: "src/query-a.go", matchPath: "src/query-a.go"},
|
||||
{path: "src/query-b.go", matchPath: "src/query-b.go"},
|
||||
{path: "src/unrelated.go", matchPath: "src/unrelated.go"},
|
||||
}
|
||||
|
||||
results := searchFileCandidates(candidates, "query", math.MaxInt32)
|
||||
|
||||
if len(results) != 2 {
|
||||
t.Fatalf("len(results) = %d, want the 2 matching candidates", len(results))
|
||||
}
|
||||
if cap(results) > len(candidates) {
|
||||
t.Fatalf("cap(results) = %d, want no more than the %d candidates searched",
|
||||
cap(results), len(candidates))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSearchFileCandidatesDefaultsNonPositiveLimit(t *testing.T) {
|
||||
candidates := make([]fileSearchCandidate, 0, fileSearchDefaultLimit+5)
|
||||
for i := range fileSearchDefaultLimit + 5 {
|
||||
|
||||
Reference in New Issue
Block a user