Skip to content

Commit 6a77a8f

Browse files
Vikhyath Mondreticlaude
andcommitted
fix(file): reject overflowed repeat bounds, describe what search covers
An upper bound too large for `Number` arrives as Infinity, and the exception that lets `{n,}` skip the repeat cap could not tell the two apart — so `needle{1,<400 digits>}` passed the cap that `needle{1,5000}` fails. The quantifier now records whether a bound was written at all, and a written one must be at or under the cap however large it is. The tool promised every active workspace file. It searches what the index currently holds: a file still pending, failed, or skipped as unsupported is not searched, and an agent reading "every file" would take an empty result as proof of absence. Both descriptions now say so and point at `complete` and `indexStatus`, which already carry the detail. The declared query description spoke only for regex mode, which is what the catalog and the generated docs render — so a builder using exact matching was told to write a regular expression and to obey a rule that does not apply to them. It now names both readings; the runtime schema is still enriched with whichever is in force. The docs overview said literal text, which stopped being true when regex became the default. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 884e825 commit 6a77a8f

5 files changed

Lines changed: 34 additions & 16 deletions

File tree

‎apps/docs/content/docs/integrations/file.mdx‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ The File block is a built-in Sim block for working with files stored in the work
1616
With the File block, you can:
1717

1818
- **Read and extract content**: Load workspace file objects and extract their text content
19-
- **Search workspace content**: Find literal text across indexed active workspace files with bounded line-level results
19+
- **Search workspace content**: Match a regular expression, or an exact piece of text, against the indexed lines of active workspace files with bounded line-level results
2020
- **Fetch from URLs**: Retrieve and parse files from external URLs with custom headers
2121
- **Write and append**: Create new workspace files or append content to existing ones
2222
- **Compress and decompress**: Bundle files into a .zip archive or extract an archive into the workspace
@@ -70,13 +70,13 @@ Extract the text content of one or more workspace files from selected file objec
7070

7171
### File Search
7272

73-
Search every active workspace file for lines matching a regular expression, and return each matching line once with its file ID and line number.
73+
Search the indexed text of active workspace files for lines matching a regular expression, and return each matching line once with its file ID and line number. Coverage is what the index currently holds, so check "complete" and "indexStatus" before concluding that something is absent.
7474

7575
#### Input
7676

7777
| Parameter | Type | Required | Description |
7878
| --------- | ---- | -------- | ----------- |
79-
| `query` | string | Yes | A regular expression matched against each line, 3-512 characters. Supports "." "*" "+" "?" "\{n,m\}" and their lazy forms, character classes such as "\[a-z\]" and "\[^0-9\]", the classes \d \w \s and \D \W \S, alternation "\|", groups "\(...\)" and "\(?:...\)", the anchors "^" and "$", and the word boundary \b. Lookahead, lookbehind, backreferences, named groups, inline flags such as "\(?i\)", \p\{...\} and POSIX "\[\[:alpha:\]\]" classes are not supported, and a pattern cannot span a line break. The pattern must contain at least 3 consecutive literal characters that every match will include — write "error \d+" rather than "\w+ \d+". Escape any metacharacter you mean literally. Matching is case-insensitive until the pattern contains an uppercase letter you are searching for; uppercase inside an escape or a character class, such as \D or \[A-Z\], does not make it case-sensitive. |
79+
| `query` | string | Yes | A regular expression matched against each line, 3-512 characters. Supports "." "*" "+" "?" "\{n,m\}" and their lazy forms, character classes such as "\[a-z\]" and "\[^0-9\]", the classes \d \w \s and \D \W \S, alternation "\|", groups "\(...\)" and "\(?:...\)", the anchors "^" and "$", and the word boundary \b. Lookahead, lookbehind, backreferences, named groups, inline flags such as "\(?i\)", \p\{...\} and POSIX "\[\[:alpha:\]\]" classes are not supported, and a pattern cannot span a line break. The pattern must contain at least 3 consecutive literal characters that every match will include — write "error \d+" rather than "\w+ \d+". Escape any metacharacter you mean literally. Matching is case-insensitive until the pattern contains an uppercase letter you are searching for; uppercase inside an escape or a character class, such as \D or \[A-Z\], does not make it case-sensitive. When the workflow builder sets Match to exact instead, the query is matched verbatim and no metacharacter needs escaping. |
8080
| `mode` | string | No | How the query is read, chosen by the workflow builder: "regex" \(default\) as a regular expression, or "exact" as verbatim text. |
8181
| `maxResults` | number | No | Hard result cap configured by the workflow builder \(1-200, default 50\). |
8282

‎apps/sim/lib/workspace-files/search/regex.test.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ describe('analyzeFileSearchRegex', () => {
107107
['foo{2,1}', /counts down/],
108108
['a{1,5000}bcd', /exceeds 1000/],
109109
['a{5000,}bcd', /exceeds 1000/],
110+
[`needle{1,1${'0'.repeat(400)}}`, /exceeds 1000/],
110111
['foo{bar}', /Unescaped "\{"/],
111112
])('rejects %s', (source, message) => {
112113
expect(() => analyzeFileSearchRegex(source)).toThrow(FileSearchPatternError)

‎apps/sim/lib/workspace-files/search/regex.ts‎

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,12 @@ function repeat(atom: LiteralGuarantee, min: number, max: number): LiteralGuaran
216216
interface Quantifier {
217217
min: number
218218
max: number
219+
/**
220+
* Whether an upper bound was written at all. `{n,}` has none to cap, but a
221+
* bound too large for `Number` also arrives as `Infinity` — without this the
222+
* two are indistinguishable and an overflowed bound is read as open-ended.
223+
*/
224+
boundedAbove: boolean
219225
}
220226

221227
/**
@@ -478,15 +484,15 @@ class FileSearchRegexParser {
478484
const character = this.peek()
479485
if (character === '*') {
480486
this.index += 1
481-
return { min: 0, max: Number.POSITIVE_INFINITY }
487+
return { min: 0, max: Number.POSITIVE_INFINITY, boundedAbove: false }
482488
}
483489
if (character === '+') {
484490
this.index += 1
485-
return { min: 1, max: Number.POSITIVE_INFINITY }
491+
return { min: 1, max: Number.POSITIVE_INFINITY, boundedAbove: false }
486492
}
487493
if (character === '?') {
488494
this.index += 1
489-
return { min: 0, max: 1 }
495+
return { min: 0, max: 1, boundedAbove: true }
490496
}
491497
if (character !== '{') return null
492498
const bounded = this.readQuantifierAt(this.index)
@@ -497,13 +503,15 @@ class FileSearchRegexParser {
497503
}
498504
/**
499505
* `{n,}` has no upper bound to cap — it is `+` with a floor, and both engines
500-
* expand it the same way — so only a stated maximum is measured against the
506+
* expand it the same way — so only a written maximum is measured against the
501507
* cap. The minimum is always measured, since that is what an expansion
502-
* actually unrolls.
508+
* actually unrolls. A written maximum that is not at or under the cap fails,
509+
* which covers one too large for `Number` to hold as well as one merely too
510+
* big.
503511
*/
504512
if (
505513
bounded.min > FILE_SEARCH_PATTERN_MAX_REPEAT ||
506-
(Number.isFinite(bounded.max) && bounded.max > FILE_SEARCH_PATTERN_MAX_REPEAT)
514+
(bounded.boundedAbove && !(bounded.max <= FILE_SEARCH_PATTERN_MAX_REPEAT))
507515
) {
508516
throw new FileSearchPatternError(
509517
`Repeat count at position ${this.index + 1} exceeds ${FILE_SEARCH_PATTERN_MAX_REPEAT}`
@@ -522,9 +530,10 @@ class FileSearchRegexParser {
522530
const match = /^\{(\d+)(,(\d*)?)?\}/.exec(this.source.slice(start))
523531
if (!match) return null
524532
const min = Number(match[1])
525-
const max =
526-
match[2] === undefined ? min : match[3] ? Number(match[3]) : Number.POSITIVE_INFINITY
527-
return { min, max, end: start + match[0].length }
533+
const exact = match[2] === undefined
534+
const boundedAbove = exact || Boolean(match[3])
535+
const max = exact ? min : match[3] ? Number(match[3]) : Number.POSITIVE_INFINITY
536+
return { min, max, boundedAbove, end: start + match[0].length }
528537
}
529538
}
530539

‎apps/sim/tools/file/search.ts‎

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,9 @@ interface FileSearchResponse extends ToolResponse {
2121
*/
2222
const TOOL_DESCRIPTIONS: Record<FileSearchMode, string> = {
2323
regex:
24-
'Search every active workspace file for lines matching a regular expression, and return each matching line once with its file ID and line number.',
24+
'Search the indexed text of active workspace files for lines matching a regular expression, and return each matching line once with its file ID and line number. Coverage is what the index currently holds, so check "complete" and "indexStatus" before concluding that something is absent.',
2525
exact:
26-
'Search every active workspace file for lines containing an exact piece of text, and return each matching line once with its file ID and line number.',
26+
'Search the indexed text of active workspace files for lines containing an exact piece of text, and return each matching line once with its file ID and line number. Coverage is what the index currently holds, so check "complete" and "indexStatus" before concluding that something is absent.',
2727
}
2828

2929
const QUERY_DESCRIPTIONS: Record<FileSearchMode, string> = {
@@ -33,6 +33,14 @@ const QUERY_DESCRIPTIONS: Record<FileSearchMode, string> = {
3333
'The exact text to find, 3-512 characters. It is matched verbatim: ".", "*", "(" and every other regular-expression metacharacter is searched for as itself, so nothing needs escaping. Matching is case-insensitive until the text contains an uppercase letter, which makes it case-sensitive.',
3434
}
3535

36+
/**
37+
* What a consumer sees before the mode is known — the catalog, the generated
38+
* docs, and a block saved before Match existed. Unlike the runtime schema it is
39+
* never enriched with the mode in force, so it has to name both readings rather
40+
* than describe only the default.
41+
*/
42+
const DECLARED_QUERY_DESCRIPTION = `${QUERY_DESCRIPTIONS.regex} When the workflow builder sets Match to exact instead, the query is matched verbatim and no metacharacter needs escaping.`
43+
3644
export const fileSearchTool: InternalToolConfig<FileSearchParams, FileSearchResponse> = {
3745
id: 'file_search',
3846
name: 'File Search',
@@ -43,7 +51,7 @@ export const fileSearchTool: InternalToolConfig<FileSearchParams, FileSearchResp
4351
type: 'string',
4452
required: true,
4553
visibility: 'user-or-llm',
46-
description: QUERY_DESCRIPTIONS.regex,
54+
description: DECLARED_QUERY_DESCRIPTION,
4755
},
4856
mode: {
4957
type: 'string',

‎apps/sim/tools/generated/tool-metadata.ts‎

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)