diff --git a/CLAUDE.md b/CLAUDE.md index 9d0749d..d24dd9f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -85,13 +85,26 @@ Use the demo repository for testing auto-tagging and webhook functionality: ## Features ### Auto-Tagging -The system automatically analyzes new issues and applies appropriate labels based on: +The system automatically analyzes new issues and applies appropriate labels using a secure, minimal-permission approach: + +**Security Features:** +- **Minimal Tool Access**: Uses only `Read` and `GitHub` tools (no file editing or bash execution) +- **Dedicated Container**: Runs in specialized container with restricted entrypoint script +- **CLI-Based**: Uses `gh` CLI commands directly instead of JSON parsing for better reliability + +**Label Categories:** - **Priority**: critical, high, medium, low - **Type**: bug, feature, enhancement, documentation, question, security - **Complexity**: trivial, simple, moderate, complex - **Component**: api, frontend, backend, database, auth, webhook, docker -When an issue is opened, Claude analyzes the title and description to suggest intelligent labels, with keyword-based fallback for reliability. +**Process Flow:** +1. New issue triggers `issues.opened` webhook +2. Dedicated Claude container starts with `claudecode-tagging-entrypoint.sh` +3. Claude analyzes issue content using minimal tools +4. Labels applied directly via `gh issue edit --add-label` commands +5. No comments posted (silent operation) +6. Fallback to keyword-based labeling if CLI approach fails ### Automated PR Review The system automatically triggers comprehensive PR reviews when all checks pass: @@ -119,20 +132,32 @@ The system automatically triggers comprehensive PR reviews when all checks pass: - `awsCredentialProvider.js` - Secure AWS credential management - `sanitize.js` - Input sanitization and security -### Execution Modes -- **Direct mode**: Runs Claude Code CLI locally -- **Container mode**: Runs Claude in isolated Docker containers with elevated privileges +### Execution Modes & Security Architecture +The system uses different execution modes based on operation type: -### DevContainer Configuration -The repository includes a `.devcontainer` configuration that allows Claude Code to run with: +**Operation Types:** +- **Auto-tagging**: Minimal permissions (`Read`, `GitHub` tools only) +- **PR Review**: Standard permissions (full tool set) +- **Default**: Standard permissions (full tool set) + +**Security Features:** +- **Tool Allowlists**: Each operation type uses specific tool restrictions +- **Dedicated Entrypoints**: Separate container entrypoint scripts for different operations +- **No Dangerous Permissions**: System avoids `--dangerously-skip-permissions` flag +- **Container Isolation**: Docker containers with minimal required capabilities + +**Container Entrypoints:** +- `claudecode-tagging-entrypoint.sh`: Minimal tools for auto-tagging (`--allowedTools Read,GitHub`) +- `claudecode-entrypoint.sh`: Full tools for general operations (`--allowedTools Bash,Create,Edit,Read,Write,GitHub`) + +**DevContainer Configuration:** +The repository includes a `.devcontainer` configuration for development: - Privileged mode for system-level access - Network capabilities (NET_ADMIN, NET_RAW) for firewall management - System capabilities (SYS_TIME, DAC_OVERRIDE, AUDIT_WRITE, SYS_ADMIN) - Docker socket mounting for container management - Automatic firewall initialization via post-create command -This configuration enables the use of `--dangerously-skip-permissions` flag when running Claude Code CLI. - ### Workflow 1. GitHub comment with bot mention (configured via BOT_USERNAME) triggers a webhook event 2. Express server receives the webhook at `/api/webhooks/github` diff --git a/scripts/runtime/claudecode-tagging-entrypoint.sh b/scripts/runtime/claudecode-tagging-entrypoint.sh new file mode 100755 index 0000000..d45ecec --- /dev/null +++ b/scripts/runtime/claudecode-tagging-entrypoint.sh @@ -0,0 +1,79 @@ +#!/bin/bash +set -e + +# Minimal entrypoint for auto-tagging workflow +# Only allows Read and GitHub tools for security + +# Environment variables (passed from service) +# Simply reference the variables directly - no need to reassign +# They are already available in the environment + +# Ensure workspace directory exists and has proper permissions +mkdir -p /workspace +chown -R node:node /workspace + +# Configure GitHub authentication +if [ -n "${GITHUB_TOKEN}" ]; then + export GH_TOKEN="${GITHUB_TOKEN}" + echo "${GITHUB_TOKEN}" | sudo -u node gh auth login --with-token + sudo -u node gh auth setup-git +else + echo "No GitHub token provided, skipping GitHub authentication" +fi + +# Clone the repository as node user (needed for context) +if [ -n "${GITHUB_TOKEN}" ] && [ -n "${REPO_FULL_NAME}" ]; then + echo "Cloning repository ${REPO_FULL_NAME}..." >&2 + sudo -u node git clone "https://x-access-token:${GITHUB_TOKEN}@github.com/${REPO_FULL_NAME}.git" /workspace/repo >&2 + cd /workspace/repo +else + echo "Skipping repository clone - missing GitHub token or repository name" >&2 + cd /workspace +fi + +# Checkout main branch (tagging doesn't need specific branches) +echo "Using main branch" >&2 +sudo -u node git checkout main >&2 || sudo -u node git checkout master >&2 + +# Configure git for minimal operations +sudo -u node git config --global user.email "${BOT_EMAIL:-claude@example.com}" +sudo -u node git config --global user.name "${BOT_USERNAME:-ClaudeBot}" + +# Configure Anthropic API key +export ANTHROPIC_API_KEY="${ANTHROPIC_API_KEY}" + +# Create response file with proper permissions +RESPONSE_FILE="/workspace/response.txt" +touch "${RESPONSE_FILE}" +chown node:node "${RESPONSE_FILE}" + +# Run Claude Code with minimal tools for auto-tagging +echo "Running Claude Code for auto-tagging..." >&2 + +# Check if command exists +if [ -z "${COMMAND}" ]; then + echo "ERROR: No command provided. COMMAND environment variable is empty." | tee -a "${RESPONSE_FILE}" >&2 + exit 1 +fi + +# Log the command length for debugging +echo "Command length: ${#COMMAND}" >&2 + +# Run Claude Code with minimal tool set: Read (for repository context) and GitHub (for label operations) +sudo -u node -E env \ + HOME="/home/node" \ + PATH="/usr/local/bin:/usr/local/share/npm-global/bin:$PATH" \ + ANTHROPIC_API_KEY="${ANTHROPIC_API_KEY}" \ + GH_TOKEN="${GITHUB_TOKEN}" \ + /usr/local/share/npm-global/bin/claude \ + --allowedTools Read,GitHub \ + --print "${COMMAND}" \ + > "${RESPONSE_FILE}" 2>&1 + +# Check for errors +if [ $? -ne 0 ]; then + echo "ERROR: Claude Code execution failed. See logs for details." | tee -a "${RESPONSE_FILE}" >&2 +fi + +# Output the response +cat "${RESPONSE_FILE}" \ No newline at end of file diff --git a/src/controllers/githubController.js b/src/controllers/githubController.js index 8132549..29520f2 100644 --- a/src/controllers/githubController.js +++ b/src/controllers/githubController.js @@ -127,73 +127,48 @@ async function handleWebhook(req, res) { ); try { - // Process the issue with Claude for automatic tagging - const tagCommand = `Analyze this issue and apply appropriate labels: + // Process the issue with Claude for automatic tagging using CLI-based approach + const tagCommand = `Analyze this GitHub issue and apply appropriate labels using GitHub CLI commands. -Title: ${issue.title} -Description: ${issue.body || 'No description provided'} +Issue Details: +- Title: ${issue.title} +- Description: ${issue.body || 'No description provided'} +- Issue Number: ${issue.number} -Available labels: -- Priority: critical, high, medium, low -- Type: bug, feature, enhancement, documentation, question, security -- Complexity: trivial, simple, moderate, complex -- Component: api, frontend, backend, database, auth, webhook, docker +Instructions: +1. First run 'gh label list' to see what labels are available in this repository +2. Analyze the issue content to determine appropriate labels from these categories: + - Priority: critical, high, medium, low + - Type: bug, feature, enhancement, documentation, question, security + - Complexity: trivial, simple, moderate, complex + - Component: api, frontend, backend, database, auth, webhook, docker +3. Apply the labels using: gh issue edit ${issue.number} --add-label "label1,label2,label3" +4. Do NOT comment on the issue - only apply labels silently -Return ONLY a JSON object with the labels to apply: -{ - "labels": ["priority:medium", "type:feature", "complexity:simple", "component:api"] -}`; +Complete the auto-tagging task using only GitHub CLI commands.`; - logger.info('Sending issue to Claude for auto-tagging analysis'); + logger.info('Sending issue to Claude for CLI-based auto-tagging'); const claudeResponse = await claudeService.processCommand({ repoFullName: repo.full_name, issueNumber: issue.number, command: tagCommand, isPullRequest: false, - branchName: null + branchName: null, + operationType: 'auto-tagging' }); - // Parse Claude's response and apply labels - try { - // Extract JSON from Claude's response (it might have additional text) using safer approach - const jsonStart = claudeResponse.indexOf('{'); - const jsonEnd = claudeResponse.lastIndexOf('}'); - if (jsonStart !== -1 && jsonEnd !== -1 && jsonEnd > jsonStart) { - const jsonString = claudeResponse.substring(jsonStart, jsonEnd + 1); - const labelSuggestion = JSON.parse(jsonString); - - if (labelSuggestion.labels && Array.isArray(labelSuggestion.labels)) { - // Apply the suggested labels - await githubService.addLabelsToIssue({ - repoOwner: repo.owner.login, - repoName: repo.name, - issueNumber: issue.number, - labels: labelSuggestion.labels - }); - - // Auto-tagging completed - no comment needed for subtlety - - const sanitizedLabels = sanitizeLabels(labelSuggestion.labels); - logger.info( - { - repo: repo.full_name, - issue: issue.number, - labelCount: sanitizedLabels.length - }, - 'Auto-tagging completed successfully' - ); - } - } - } catch (parseError) { + // With CLI-based approach, Claude handles the labeling directly + // Check if the response indicates success or if we need fallback + if (claudeResponse.includes('error') || claudeResponse.includes('failed')) { logger.warn( { - err: parseError, - claudeResponseLength: claudeResponse.length, - claudeResponsePreview: '[RESPONSE_CONTENT_REDACTED]' + repo: repo.full_name, + issue: issue.number, + responsePreview: claudeResponse.substring(0, 200) }, - 'Failed to parse Claude response for auto-tagging' + 'Claude CLI tagging may have failed, attempting fallback' ); - + // Fall back to basic tagging based on keywords const fallbackLabels = await githubService.getFallbackLabels(issue.title, issue.body); if (fallbackLabels.length > 0) { @@ -203,7 +178,17 @@ Return ONLY a JSON object with the labels to apply: issueNumber: issue.number, labels: fallbackLabels }); + logger.info('Applied fallback labels successfully'); } + } else { + logger.info( + { + repo: repo.full_name, + issue: issue.number, + responseLength: claudeResponse.length + }, + 'Auto-tagging completed with CLI approach' + ); } return res.status(200).json({ diff --git a/src/services/claudeService.js b/src/services/claudeService.js index 2d68ff5..41da643 100644 --- a/src/services/claudeService.js +++ b/src/services/claudeService.js @@ -30,6 +30,7 @@ if (!BOT_USERNAME) { * @param {string} options.command - The command to process with Claude * @param {boolean} [options.isPullRequest=false] - Whether this is a pull request * @param {string} [options.branchName] - The branch name for pull requests + * @param {string} [options.operationType='default'] - Operation type: 'auto-tagging', 'pr-review', or 'default' * @returns {Promise} - Claude's response */ async function processCommand({ @@ -37,7 +38,8 @@ async function processCommand({ issueNumber, command, isPullRequest = false, - branchName = null + branchName = null, + operationType = 'default' }) { try { logger.info( @@ -91,12 +93,59 @@ For real functionality, please configure valid GitHub and Claude API tokens.`; }); } + // Select appropriate entrypoint script based on operation type + let entrypointScript; + switch (operationType) { + case 'auto-tagging': + entrypointScript = '/scripts/runtime/claudecode-tagging-entrypoint.sh'; + logger.info({ operationType }, 'Using minimal tools for auto-tagging operation'); + break; + case 'pr-review': + case 'default': + default: + entrypointScript = '/scripts/runtime/claudecode-entrypoint.sh'; + logger.info({ operationType }, 'Using full tool set for standard operation'); + break; + } + // Create unique container name (sanitized to prevent command injection) const sanitizedRepoName = repoFullName.replace(/[^a-zA-Z0-9\-_]/g, '-'); const containerName = `claude-${sanitizedRepoName}-${Date.now()}`; - // Create the full prompt with context and instructions - const fullPrompt = `You are Claude, an AI assistant responding to a GitHub ${isPullRequest ? 'pull request' : 'issue'} via the ${BOT_USERNAME} webhook. + // Create the full prompt with context and instructions based on operation type + let fullPrompt; + + if (operationType === 'auto-tagging') { + fullPrompt = `You are Claude, an AI assistant analyzing a GitHub issue for automatic label assignment. + +**Context:** +- Repository: ${repoFullName} +- Issue Number: #${issueNumber} +- Operation: Auto-tagging (Read-only + Label assignment) + +**Available Tools:** +- Read: Access repository files and issue content +- GitHub: Use 'gh' CLI for label operations only + +**Task:** +Analyze the issue and apply appropriate labels using GitHub CLI commands. Use these categories: +- Priority: critical, high, medium, low +- Type: bug, feature, enhancement, documentation, question, security +- Complexity: trivial, simple, moderate, complex +- Component: api, frontend, backend, database, auth, webhook, docker + +**Process:** +1. First run 'gh label list' to see available labels +2. Analyze the issue content +3. Use 'gh issue edit #{issueNumber} --add-label "label1,label2,label3"' to apply labels +4. Do NOT comment on the issue - only apply labels + +**User Request:** +${command} + +Complete the auto-tagging task using only the minimal required tools.`; + } else { + fullPrompt = `You are Claude, an AI assistant responding to a GitHub ${isPullRequest ? 'pull request' : 'issue'} via the ${BOT_USERNAME} webhook. **Context:** - Repository: ${repoFullName} @@ -132,6 +181,7 @@ For real functionality, please configure valid GitHub and Claude API tokens.`; ${command} Please complete this task fully and autonomously.`; + } // Prepare environment variables for the container const envVars = { @@ -139,6 +189,7 @@ Please complete this task fully and autonomously.`; ISSUE_NUMBER: issueNumber || '', IS_PULL_REQUEST: isPullRequest ? 'true' : 'false', BRANCH_NAME: branchName || '', + OPERATION_TYPE: operationType, COMMAND: fullPrompt, GITHUB_TOKEN: githubToken, ANTHROPIC_API_KEY: secureCredentials.get('ANTHROPIC_API_KEY') @@ -220,8 +271,8 @@ Please complete this task fully and autonomously.`; } }); - // Add the image name as the final argument - dockerArgs.push(dockerImageName); + // Add the image name and custom entrypoint + dockerArgs.push('--entrypoint', entrypointScript, dockerImageName); // Create sanitized version for logging (remove sensitive values) const sanitizedArgs = dockerArgs.map(arg => { diff --git a/test-results/jest/results.xml b/test-results/jest/results.xml index 10b860a..7b59c32 100644 --- a/test-results/jest/results.xml +++ b/test-results/jest/results.xml @@ -1,11 +1,11 @@ - - - + + + - + - + @@ -22,60 +22,60 @@ - - + + - - - - - - - - - - - + - + - + - + - + - + - + + + + + + + + + + + - + - + - - + + - + - + - + - + - +