diff --git a/.github/issue-assistant/src/security.js b/.github/issue-assistant/src/security.js index a4d3f9b9..60bdace6 100644 --- a/.github/issue-assistant/src/security.js +++ b/.github/issue-assistant/src/security.js @@ -227,21 +227,9 @@ async function validateRequest({ errors.push('Rate limit exceeded'); } - if (comment && maxBotResponses !== undefined) { - const { data: comments } = await github.rest.issues.listComments({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issue.number - }); - - const botComments = comments.filter(c => - c.body && c.body.includes('') - ); - - if (botComments.length >= maxBotResponses) { - errors.push('Maximum bot responses reached'); - } - } + // Note: Bot response count per issue is now validated in the conversation-state step + // of the workflow, not here. This avoids redundant validation and keeps state + // management centralized. return { shouldRespond: errors.length === 0, diff --git a/.github/workflows/issue-assistant.yml b/.github/workflows/issue-assistant.yml index d32a4f92..33b8c88d 100644 --- a/.github/workflows/issue-assistant.yml +++ b/.github/workflows/issue-assistant.yml @@ -37,7 +37,6 @@ jobs: should_respond: ${{ (steps.conversation-state.outputs.should_respond == 'true' && steps.validation.outputs.validation_passed == 'true') ? 'true' : 'false' }} conversation_state: ${{ steps.conversation-state.outputs.state }} conversation_history: ${{ steps.conversation-state.outputs.history }} - sanitized_content: ${{ steps.validation.outputs.sanitized_content }} issue_type: ${{ steps.validation.outputs.issue_type }} wiki_context: ${{ steps.wiki.outputs.context }} @@ -96,7 +95,7 @@ jobs: } // 2. Terminal states - don't respond further - if (['resolved', 'escalated', 'closed'].includes(currentState)) { + if (['resolved', 'escalated'].includes(currentState)) { console.log(`Terminal state "${currentState}" - no more responses`); core.setOutput('should_respond', 'false'); core.setOutput('state', currentState); @@ -142,19 +141,52 @@ jobs: } // === BUILD CONVERSATION HISTORY === + /** + * Sanitizes user input to prevent injection attacks and normalize formatting. + * @param {string} content - The raw user input to sanitize + * @param {number} maxLength - Maximum allowed length (default: 10000) + * @returns {string} Sanitized content, truncated if exceeds maxLength + */ + const sanitizeContent = (content, maxLength = 10000) => { + // Handle null, undefined, or empty string + if (content == null || content === '') return ''; + + // Coerce to string if needed (though content should always be a string) + const str = String(content); + + let sanitized = str + .replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '') // Remove control chars (preserves \t=tab, \n=newline, \r=CR) + .replace(/\r\n/g, '\n') // Normalize Windows line endings + .replace(/\r/g, '\n') // Normalize old Mac line endings + .replace(/[^\S\r\n]+/g, ' ') // Normalize whitespace (except newlines) + .replace(/\n{3,}/g, '\n\n') // Collapse excessive newlines + .trim(); + if (sanitized.length > maxLength) { + sanitized = sanitized.substring(0, maxLength) + '... [truncated]'; + } + return sanitized; + }; + const history = []; - // Add issue body as first message + // Add issue body as first message (sanitize it) + const issueContent = `[Issue opened] ${issue.title}\n\n${issue.body || '(no description)'}`; history.push({ role: 'user', author: issueAuthor, - content: `[Issue opened] ${issue.title}\n\n${issue.body || '(no description)'}`, + content: sanitizeContent(issueContent), timestamp: issue.created_at }); - // Add all comments in order + // Add comments from issue author and bot only (filter out other users) for (const comment of comments) { const isBot = comment.body.includes('