-
Notifications
You must be signed in to change notification settings - Fork 0
chore: sync core lib and CLAUDE.md from agent-core #27
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -440,7 +440,9 @@ const agentPatterns = { | |||||
| // Look for hardcoded .claude/ references | ||||||
| const hasHardcoded = /\.claude\//.test(content); | ||||||
| // Exclude if using AI_STATE_DIR | ||||||
| const usesEnvVar = /AI_STATE_DIR|\$\{.*STATE.*\}/i.test(content); | ||||||
| // ReDoS fix: bound the .* runs to non-brace chars so they cannot cross } | ||||||
| // and cannot backtrack; matches the same ${...STATE...} expressions. | ||||||
| const usesEnvVar = /AI_STATE_DIR|\$\{[^}]*STATE[^}]*\}/i.test(content); | ||||||
|
|
||||||
| if (hasHardcoded && !usesEnvVar) { | ||||||
| return { | ||||||
|
|
@@ -494,8 +496,12 @@ const agentPatterns = { | |||||
|
|
||||||
| // Check if has code blocks or lists but no XML | ||||||
| const hasCodeBlocks = /```[\s\S]+?```/.test(content); | ||||||
| const hasLists = /^[-*]\s+.+$/m.test(content); | ||||||
| const hasXML = /<\w+>[\s\S]*?<\/\w+>/.test(content); | ||||||
| // ReDoS fix: bound the \s+ and line-content runs; line-anchored so this still | ||||||
| // detects any "- item" / "* item" list line as before. | ||||||
| const hasLists = /^[-*]\s{1,1000}[^\n]{1,2000}$/m.test(content); | ||||||
| // ReDoS fix: bound the unbounded [\s\S]*? so an unterminated <tag> cannot | ||||||
| // drive polynomial backtracking; 50k chars covers any realistic XML block. | ||||||
| const hasXML = /<\w+>[\s\S]{0,50000}?<\/\w+>/.test(content); | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The regex By bounding it to We should revert to the original unbounded lazy quantifier which is safe and robust.
Suggested change
|
||||||
| const sectionCount = (content.match(/^##\s+/gm) || []).length; | ||||||
|
|
||||||
| // Complex content without XML | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -54,9 +54,12 @@ const PATTERN_HEURISTICS = { | |||||
| const contentLower = content.toLowerCase(); | ||||||
|
|
||||||
| // Check if file is pattern documentation describing vague language detection | ||||||
| // ReDoS fix: the .* runs never matched across newlines (. excludes \n), so | ||||||
| // bounding them to [^\n]{0,N} keeps the same "within one line, in order" | ||||||
| // semantics while removing the polynomial multi-.* backtracking. | ||||||
| const isPatternDoc = | ||||||
| /pattern.*detect.*usually|example.*vague|fuzzy.*language.*like/i.test(content) || | ||||||
| /vague.*terms.*like|"usually".*"sometimes"/i.test(content); | ||||||
| /pattern[^\n]{0,500}detect[^\n]{0,500}usually|example[^\n]{0,500}vague|fuzzy[^\n]{0,500}language[^\n]{0,500}like/i.test(content) || | ||||||
| /vague[^\n]{0,500}terms[^\n]{0,500}like|"usually"[^\n]{0,500}"sometimes"/i.test(content); | ||||||
|
Comment on lines
60
to
+62
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The original regexes used Instead of using complex regular expressions with arbitrary limits, we can split the content into lines and check for the presence of these words in order using simple, highly performant string operations. This completely eliminates any backtracking/ReDoS risk and removes all arbitrary line-length limits. const lines = content.toLowerCase().split('\n');
const hasWordsInOrder = (str, words) => {
let index = 0;
for (const word of words) {
index = str.indexOf(word, index);
if (index === -1) return false;
index += word.length;
}
return true;
};
const isPatternDoc = lines.some(line =>
hasWordsInOrder(line, ['pattern', 'detect', 'usually']) ||
hasWordsInOrder(line, ['example', 'vague']) ||
hasWordsInOrder(line, ['fuzzy', 'language', 'like']) ||
hasWordsInOrder(line, ['vague', 'terms', 'like']) ||
hasWordsInOrder(line, ['"usually"', '"sometimes"'])
); |
||||||
|
|
||||||
| if (isPatternDoc) { | ||||||
| return { | ||||||
|
|
@@ -129,7 +132,10 @@ const PATTERN_HEURISTICS = { | |||||
| const isOrchestrator = | ||||||
| fileNameLower.includes('orchestrator') || | ||||||
| fileNameLower.includes('coordinator') || | ||||||
| /Task\s*\(\s*\{[\s\S]*subagent_type/i.test(content); | ||||||
| // ReDoS fix: bound the unbounded [\s\S]* so a "Task({" with no following | ||||||
| // subagent_type cannot drive polynomial backtracking; 50k chars covers any | ||||||
| // realistic Task(...) call body. | ||||||
| /Task\s{0,100}\(\s{0,100}\{[\s\S]{0,50000}subagent_type/i.test(content); | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The regex We can check if
Suggested change
|
||||||
|
|
||||||
| if (isOrchestrator) { | ||||||
| return { | ||||||
|
|
@@ -140,7 +146,9 @@ const PATTERN_HEURISTICS = { | |||||
|
|
||||||
| // Check if workflow command that invokes agents | ||||||
| const isWorkflowCommand = | ||||||
| /spawn.*agent|invoke.*agent|Task\s*\(\s*\{/i.test(content) && | ||||||
| // ReDoS fix: bound the within-line .* runs and \s* runs ([^\n] == . here) | ||||||
| // to keep the same matches without polynomial backtracking. | ||||||
| /spawn[^\n]{0,500}agent|invoke[^\n]{0,500}agent|Task\s{0,100}\(\s{0,100}\{/i.test(content) && | ||||||
| fileNameLower.endsWith('.md'); | ||||||
|
|
||||||
| if (isWorkflowCommand) { | ||||||
|
|
@@ -159,9 +167,11 @@ const PATTERN_HEURISTICS = { | |||||
| */ | ||||||
| missing_output_format: (finding, content, context) => { | ||||||
| // Check if content spawns subagents with their own output specs | ||||||
| // ReDoS fix: bound the within-line .* and \s* runs ([^\n] == . here) so the | ||||||
| // same membership matches hold without polynomial backtracking. | ||||||
| const spawnsSubagent = | ||||||
| /subagent_type|spawn.*agent|Task\s*\(\s*\{/i.test(content) || | ||||||
| /enhance:.*-enhancer|enhance:.*-reporter/i.test(content); | ||||||
| /subagent_type|spawn[^\n]{0,500}agent|Task\s{0,100}\(\s{0,100}\{/i.test(content) || | ||||||
| /enhance:[^\n]{0,500}-enhancer|enhance:[^\n]{0,500}-reporter/i.test(content); | ||||||
|
|
||||||
| if (spawnsSubagent) { | ||||||
| return { | ||||||
|
|
@@ -180,7 +190,9 @@ const PATTERN_HEURISTICS = { | |||||
| missing_constraints: (finding, content, context) => { | ||||||
| // Check for constraint section presence | ||||||
| const hasConstraintSection = | ||||||
| /##\s*What\s+.*MUST\s+NOT\s+Do/i.test(content) || | ||||||
| // ReDoS fix: bound the within-line .* and \s runs ([^\n] == . here) so the | ||||||
| // "## What ... MUST NOT Do" heading still matches without backtracking. | ||||||
| /##\s{0,100}What\s{1,100}[^\n]{0,500}MUST\s{1,100}NOT\s{1,100}Do/i.test(content) || | ||||||
| /##\s*Constraints/i.test(content) || | ||||||
| /<constraints>/i.test(content) || | ||||||
| /##\s*Critical\s+Constraints/i.test(content) || | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -24,7 +24,9 @@ const docsPatterns = { | |||||
| if (!content || typeof content !== 'string') return null; | ||||||
|
|
||||||
| // Find markdown links | ||||||
| const linkRegex = /\[([^\]]+)\]\(([^)]+)\)/g; | ||||||
| // ReDoS fix: bound the negated-class captures so the matcher is linear; | ||||||
| // bounds far exceed any realistic markdown link, so matches are unchanged. | ||||||
| const linkRegex = /\[([^\]]{1,2000})\]\(([^)]{1,4000})\)/g; | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The original regex By bounding them to
Suggested change
|
||||||
| const brokenLinks = []; | ||||||
| let match; | ||||||
|
|
||||||
|
|
@@ -40,7 +42,9 @@ const docsPatterns = { | |||||
| if (linkTarget.startsWith('#')) { | ||||||
| const anchorId = linkTarget.slice(1).toLowerCase(); | ||||||
| // Generate expected heading anchors from content | ||||||
| const headings = content.match(/^#{1,6}\s+(.+)$/gm) || []; | ||||||
| // ReDoS fix: bound the \s+ run; line-anchored (.+) cannot cross newlines | ||||||
| // so the same headings match as before. | ||||||
| const headings = content.match(/^#{1,6}\s{1,1000}(.+)$/gm) || []; | ||||||
| const anchors = headings.map(h => { | ||||||
| return h.replace(/^#{1,6}\s+/, '') | ||||||
| .toLowerCase() | ||||||
|
|
||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The original regular expression
/(?:const|let)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=\s*(?:async\s*)?\([^)]*\)\s*=>/gdoes not possess a polynomial or exponential backtracking (ReDoS) vulnerability. Negated character classes like[^)]*followed by their delimiter)match linearly and do not backtrack overlappingly.By introducing an arbitrary limit of
[^)]{0,2000}, this pattern will silently fail to match arrow functions with parameter lists longer than 2000 characters. In TypeScript files (which this collector scans, e.g.,.tsand.tsx), parameter lists with complex object destructuring, default values, or type annotations can easily exceed 2000 characters.We should revert to the original clean regex which is safe and robust.