From 7a80cdf84dd1f48041c0752b8de8fcef5ac6c406 Mon Sep 17 00:00:00 2001 From: openhands Date: Wed, 19 Nov 2025 13:12:48 +0000 Subject: [PATCH 1/5] Integrate OpenHands secrets manager and GitHub API - Add secrets manager integration to BaseAgent for secure GITHUB_TOKEN handling - Implement GitHub API methods for creating PRs programmatically - Update CVE solver to use secrets manager and GitHub API instead of browser automation - Add comprehensive documentation for secrets manager usage - Ensure backward compatibility with fallback mechanisms Co-authored-by: openhands --- docs/SECRETS_AND_GITHUB_API.md | 324 +++++++++++++++++++++++++++++++++ src/agents/base_agent.py | 203 +++++++++++++++++++-- src/agents/cve_solver.py | 100 ++++++++-- 3 files changed, 594 insertions(+), 33 deletions(-) create mode 100644 docs/SECRETS_AND_GITHUB_API.md diff --git a/docs/SECRETS_AND_GITHUB_API.md b/docs/SECRETS_AND_GITHUB_API.md new file mode 100644 index 0000000..b35b3df --- /dev/null +++ b/docs/SECRETS_AND_GITHUB_API.md @@ -0,0 +1,324 @@ +# OpenHands Secrets Manager & GitHub API Integration + +This document explains how to properly use the OpenHands secrets manager to handle sensitive data like `GITHUB_TOKEN` and how to use the GitHub API to create Pull Requests programmatically. + +## Table of Contents + +1. [Overview](#overview) +2. [Setting up the Secrets Manager](#setting-up-the-secrets-manager) +3. [Using Secrets in Conversations](#using-secrets-in-conversations) +4. [GitHub API Integration](#github-api-integration) +5. [Complete Workflow Example](#complete-workflow-example) +6. [Best Practices](#best-practices) +7. [Troubleshooting](#troubleshooting) + +## Overview + +The OpenHands SDK provides a robust secrets management system that: +- Securely stores sensitive values like API tokens +- Automatically injects secrets into bash commands +- Masks secret values in outputs and logs +- Supports both static secrets and dynamic secret sources + +## Setting up the Secrets Manager + +### 1. Import Required Components + +```python +from openhands.sdk.conversation.secret_source import StaticSecret +from pydantic import SecretStr +``` + +### 2. Create Secret Sources + +```python +def _setup_secrets(self) -> dict: + """Set up secrets for the conversation.""" + secrets = {} + + # Add GITHUB_TOKEN if available + github_token = os.getenv('GITHUB_TOKEN') + if github_token: + secrets['GITHUB_TOKEN'] = StaticSecret(value=SecretStr(github_token)) + + # Add other secrets as needed + api_key = os.getenv('API_KEY') + if api_key: + secrets['API_KEY'] = StaticSecret(value=SecretStr(api_key)) + + return secrets +``` + +### 3. Configure Conversation with Secrets + +```python +def create_agent_conversation(self) -> tuple[Agent, Conversation]: + """Create an agent and conversation instance with secrets.""" + agent = Agent( + llm=self.llm, + tools=get_default_tools(enable_browser=False), + ) + + conversation = Conversation( + agent=agent, + workspace=self.workspace, + visualizer=None, + secrets=self.secrets, # Pass secrets during creation + ) + + # Update secrets in the conversation (ensures proper registration) + if self.secrets: + conversation.update_secrets(self.secrets) + + return agent, conversation +``` + +## Using Secrets in Conversations + +### Automatic Secret Injection + +When you use a secret name in a bash command, the secrets manager automatically injects the actual value: + +```python +# This command will have $GITHUB_TOKEN automatically replaced +conversation.send_message(""" +git clone https://$GITHUB_TOKEN@github.com/owner/repo.git +""") +conversation.run() +``` + +### Secret Masking + +All secret values are automatically masked in outputs: +- Console logs show `` instead of actual values +- Terminal outputs are scrubbed of secret values +- Error messages don't expose secrets + +## GitHub API Integration + +### 1. Creating Pull Requests + +Use the GitHub API instead of browser automation for creating PRs: + +```python +def create_github_pr(self, repo_owner: str, repo_name: str, title: str, + body: str, head_branch: str, base_branch: str = "main") -> Dict[str, Any]: + """Create a GitHub Pull Request using the GitHub API.""" + github_token = os.getenv('GITHUB_TOKEN') + if not github_token: + return {"success": False, "error": "GITHUB_TOKEN not available"} + + url = f"https://api.github.com/repos/{repo_owner}/{repo_name}/pulls" + headers = { + "Authorization": f"token {github_token}", + "Accept": "application/vnd.github.v3+json", + "Content-Type": "application/json" + } + + data = { + "title": title, + "body": body, + "head": head_branch, + "base": base_branch + } + + try: + response = requests.post(url, headers=headers, json=data) + response.raise_for_status() + + pr_data = response.json() + return { + "success": True, + "pr_url": pr_data.get("html_url"), + "pr_number": pr_data.get("number"), + "pr_data": pr_data + } + except requests.exceptions.RequestException as e: + return {"success": False, "error": f"Failed to create PR: {str(e)}"} +``` + +### 2. Complete Git Workflow with Secrets + +```python +def push_changes_and_create_pr(self, conversation: Conversation, repo_dir: str, + repo_owner: str, repo_name: str, branch_name: str, + commit_message: str, pr_title: str, pr_body: str) -> Dict[str, Any]: + """Push changes to GitHub and create a Pull Request.""" + try: + # Stage, commit, and push changes (secrets automatically injected) + conversation.send_message(f""" +cd {repo_dir} +git add . +git commit -m "{commit_message}" +git push -u origin {branch_name} +""") + conversation.run() + + # Create PR using GitHub API + pr_result = self.create_github_pr( + repo_owner=repo_owner, + repo_name=repo_name, + title=pr_title, + body=pr_body, + head_branch=branch_name + ) + + return pr_result + except Exception as e: + return {"success": False, "error": f"Failed to push and create PR: {str(e)}"} +``` + +## Complete Workflow Example + +Here's a complete example of how a CVE solver agent would use secrets and GitHub API: + +```python +class CVESolver(BaseAgent): + def solve_vulnerability(self, vulnerability: Dict[str, Any], repo_url: str) -> Dict[str, Any]: + """Fix a vulnerability and create a PR.""" + + # 1. Create conversation with secrets + agent, conversation = self.create_agent_conversation() + + # 2. Clone repository (secrets automatically injected) + repo_dir = "/tmp/cve_fix" + success = self.clone_repository(repo_url, repo_dir, conversation) + if not success: + return {"success": False, "error": "Failed to clone repository"} + + # 3. Create feature branch + branch_name = f"fix/cve-{vulnerability['id']}" + self.create_github_branch(conversation, repo_dir, branch_name) + + # 4. Apply fixes using the agent + conversation.send_message(f""" +Please analyze and fix the vulnerability: {vulnerability['description']} +The vulnerable file is: {vulnerability['file']} +Apply the necessary security fixes. +""") + conversation.run() + + # 5. Push changes and create PR + repo_parts = repo_url.replace('https://github.com/', '').replace('.git', '').split('/') + repo_owner, repo_name = repo_parts[0], repo_parts[1] + + pr_result = self.push_changes_and_create_pr( + conversation=conversation, + repo_dir=repo_dir, + repo_owner=repo_owner, + repo_name=repo_name, + branch_name=branch_name, + commit_message=f"Fix {vulnerability['id']}: {vulnerability['title']}", + pr_title=f"Security Fix: {vulnerability['title']}", + pr_body=f"""## Summary +This PR fixes the security vulnerability {vulnerability['id']}. + +## Vulnerability Details +- **CVE ID**: {vulnerability['id']} +- **Severity**: {vulnerability['severity']} +- **Description**: {vulnerability['description']} + +## Changes Made +The agent has automatically applied the necessary security fixes. + +## Testing +Please review the changes and run your security tests to verify the fix. +""" + ) + + return pr_result +``` + +## Best Practices + +### 1. Secret Management +- Always use `StaticSecret` to wrap sensitive values +- Never log or print secret values directly +- Use environment variables to provide secrets to the application +- Register secrets with conversations using `update_secrets()` + +### 2. GitHub API Usage +- Use the GitHub API instead of browser automation +- Handle API rate limits and errors gracefully +- Include meaningful PR titles and descriptions +- Use proper branch naming conventions + +### 3. Error Handling +- Always check if secrets are available before using them +- Handle API failures gracefully +- Provide meaningful error messages +- Clean up resources (directories, branches) on failure + +### 4. Security +- Never expose secrets in logs or error messages +- Use HTTPS for all GitHub operations +- Validate repository URLs before cloning +- Use minimal required permissions for tokens + +## Troubleshooting + +### Common Issues + +1. **GITHUB_TOKEN not found** + ``` + Error: GITHUB_TOKEN not available + ``` + **Solution**: Set the `GITHUB_TOKEN` environment variable with a valid GitHub personal access token. + +2. **Secret not injected in commands** + ``` + Error: $GITHUB_TOKEN not replaced in git command + ``` + **Solution**: Ensure secrets are registered with the conversation using `update_secrets()`. + +3. **GitHub API authentication failed** + ``` + Error: 401 Unauthorized + ``` + **Solution**: Verify the GitHub token has the required permissions (repo access). + +4. **PR creation failed** + ``` + Error: 422 Unprocessable Entity + ``` + **Solution**: Check that the branch exists and the PR doesn't already exist. + +### Debugging Tips + +1. **Check secret registration**: + ```python + print(f"Secrets registered: {list(conversation.state.secret_registry.secret_sources.keys())}") + ``` + +2. **Verify token permissions**: + ```bash + curl -H "Authorization: token $GITHUB_TOKEN" https://api.github.com/user + ``` + +3. **Test API connectivity**: + ```python + response = requests.get("https://api.github.com/rate_limit", + headers={"Authorization": f"token {github_token}"}) + print(response.json()) + ``` + +## Running the Demo + +To see the secrets manager and GitHub API in action, run the demonstration script: + +```bash +cd /workspace/project/cve-demo +python examples/secrets_and_github_api_demo.py +``` + +This will show you: +- How secrets are configured and registered +- How automatic secret injection works +- How to use the GitHub API for PR creation +- A complete CVE fix workflow example + +## Additional Resources + +- [OpenHands SDK Documentation](https://github.com/openhands/software-agent-sdk) +- [GitHub API Documentation](https://docs.github.com/en/rest) +- [GitHub Personal Access Tokens](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token) \ No newline at end of file diff --git a/src/agents/base_agent.py b/src/agents/base_agent.py index e57d5ab..a15b4b0 100644 --- a/src/agents/base_agent.py +++ b/src/agents/base_agent.py @@ -2,9 +2,12 @@ import os import uuid +import json +import requests from typing import Dict, Any, Optional from pydantic import SecretStr from openhands.sdk import LLM, Agent, Conversation, RemoteWorkspace +from openhands.sdk.conversation.secret_source import StaticSecret from openhands.tools.preset.default import get_default_tools from config.config import Config @@ -25,6 +28,20 @@ def __init__(self, config: Config, agent_id: str, workspace: RemoteWorkspace): api_key=SecretStr(config.llm_api_key) if config.llm_api_key else None, base_url=config.llm_base_url, ) + + # Set up secrets for the conversation + self.secrets = {} + self._setup_secrets() + + def _setup_secrets(self): + """Set up secrets that will be available to the conversation.""" + # Add GITHUB_TOKEN if available + github_token = os.getenv('GITHUB_TOKEN') + if github_token: + self.secrets['GITHUB_TOKEN'] = StaticSecret(value=SecretStr(github_token)) + + # Add other common secrets as needed + # You can add more secrets here following the same pattern def create_workspace_directory(self, prefix: str) -> str: """Create a unique workspace directory.""" @@ -32,24 +49,47 @@ def create_workspace_directory(self, prefix: str) -> str: self.workspace.execute_command(f"mkdir -p {workspace_dir}") return workspace_dir - def clone_repository(self, repo_url: str, target_dir: str) -> bool: - """Clone a repository to the target directory.""" - github_token = os.getenv('GITHUB_TOKEN') - if github_token and 'github.com' in repo_url: - # Use token for GitHub repositories + def clone_repository(self, repo_url: str, target_dir: str, conversation: Optional[Conversation] = None) -> bool: + """Clone a repository to the target directory using secrets manager. + + Args: + repo_url: Git URL of the repository + target_dir: Target directory for cloning + conversation: Optional conversation instance for secret injection + + Returns: + True if clone was successful, False otherwise + """ + if 'github.com' in repo_url and 'GITHUB_TOKEN' in self.secrets: + # Use authenticated clone with token from secrets manager if repo_url.startswith('https://github.com/'): - auth_url = repo_url.replace('https://github.com/', f'https://{github_token}@github.com/') + # The GITHUB_TOKEN will be automatically injected by the secrets manager + # when the conversation executes the git clone command + auth_url = repo_url.replace('https://github.com/', 'https://$GITHUB_TOKEN@github.com/') else: auth_url = repo_url - clone_result = self.workspace.execute_command(f"cd {os.path.dirname(target_dir)} && git clone {auth_url} {os.path.basename(target_dir)}") - else: - # Clone without authentication for public repos - clone_result = self.workspace.execute_command(f"cd {os.path.dirname(target_dir)} && git clone {repo_url} {os.path.basename(target_dir)}") + + if conversation: + # Use conversation to execute with automatic secret injection + conversation.send_message(f"cd {os.path.dirname(target_dir)} && git clone {auth_url} {os.path.basename(target_dir)}") + conversation.run() + # Check if directory was created successfully + check_result = self.workspace.execute_command(f"test -d {target_dir}") + return check_result.exit_code == 0 + else: + # Fallback to direct workspace execution (less secure) + github_token = os.getenv('GITHUB_TOKEN') + if github_token: + auth_url = repo_url.replace('https://github.com/', f'https://{github_token}@github.com/') + clone_result = self.workspace.execute_command(f"cd {os.path.dirname(target_dir)} && git clone {auth_url} {os.path.basename(target_dir)}") + return clone_result.exit_code == 0 + # Clone without authentication for public repos + clone_result = self.workspace.execute_command(f"cd {os.path.dirname(target_dir)} && git clone {repo_url} {os.path.basename(target_dir)}") return clone_result.exit_code == 0 def create_agent_conversation(self) -> tuple[Agent, Conversation]: - """Create an agent and conversation instance.""" + """Create an agent and conversation instance with secrets properly configured.""" agent = Agent( llm=self.llm, tools=get_default_tools(enable_browser=False), @@ -59,15 +99,148 @@ def create_agent_conversation(self) -> tuple[Agent, Conversation]: agent=agent, workspace=self.workspace, visualizer=None, + secrets=self.secrets, # Pass secrets to the conversation ) + # Update secrets in the conversation (this ensures they're properly registered) + if self.secrets: + conversation.update_secrets(self.secrets) + return agent, conversation - def setup_git_environment(self): - """Set up git environment variables.""" + def create_github_pr(self, repo_owner: str, repo_name: str, title: str, body: str, + head_branch: str, base_branch: str = "main") -> Dict[str, Any]: + """Create a GitHub Pull Request using the GitHub API. + + Args: + repo_owner: GitHub repository owner + repo_name: GitHub repository name + title: PR title + body: PR description + head_branch: Source branch for the PR + base_branch: Target branch for the PR (default: main) + + Returns: + Dictionary with PR creation results + """ github_token = os.getenv('GITHUB_TOKEN') - if github_token: - self.workspace.execute_command(f"export GITHUB_TOKEN={github_token}") + if not github_token: + return { + "success": False, + "error": "GITHUB_TOKEN not available", + "pr_url": None + } + + url = f"https://api.github.com/repos/{repo_owner}/{repo_name}/pulls" + headers = { + "Authorization": f"token {github_token}", + "Accept": "application/vnd.github.v3+json", + "Content-Type": "application/json" + } + + data = { + "title": title, + "body": body, + "head": head_branch, + "base": base_branch + } + + try: + response = requests.post(url, headers=headers, json=data) + response.raise_for_status() + + pr_data = response.json() + return { + "success": True, + "pr_url": pr_data.get("html_url"), + "pr_number": pr_data.get("number"), + "pr_data": pr_data + } + except requests.exceptions.RequestException as e: + return { + "success": False, + "error": f"Failed to create PR: {str(e)}", + "pr_url": None + } + + def create_github_branch(self, conversation: Conversation, repo_dir: str, + branch_name: str, base_branch: str = "main") -> bool: + """Create a new branch in the repository using the conversation. + + Args: + conversation: Conversation instance with secrets configured + repo_dir: Repository directory path + branch_name: Name of the new branch + base_branch: Base branch to create from (default: main) + + Returns: + True if branch creation was successful + """ + try: + # Create and checkout new branch + conversation.send_message(f""" +cd {repo_dir} +git checkout {base_branch} +git pull origin {base_branch} +git checkout -b {branch_name} +""") + conversation.run() + + # Verify branch was created + check_result = self.workspace.execute_command(f"cd {repo_dir} && git branch --show-current") + return check_result.stdout.strip() == branch_name + except Exception: + return False + + def push_changes_and_create_pr(self, conversation: Conversation, repo_dir: str, + repo_owner: str, repo_name: str, branch_name: str, + commit_message: str, pr_title: str, pr_body: str) -> Dict[str, Any]: + """Push changes to GitHub and create a Pull Request. + + Args: + conversation: Conversation instance with secrets configured + repo_dir: Repository directory path + repo_owner: GitHub repository owner + repo_name: GitHub repository name + branch_name: Branch name to push + commit_message: Git commit message + pr_title: Pull Request title + pr_body: Pull Request description + + Returns: + Dictionary with operation results + """ + try: + # Stage, commit, and push changes + conversation.send_message(f""" +cd {repo_dir} +git add . +git commit -m "{commit_message}" +git push -u origin {branch_name} +""") + conversation.run() + + # Create PR using GitHub API + pr_result = self.create_github_pr( + repo_owner=repo_owner, + repo_name=repo_name, + title=pr_title, + body=pr_body, + head_branch=branch_name + ) + + return { + "success": pr_result["success"], + "pr_url": pr_result.get("pr_url"), + "pr_number": pr_result.get("pr_number"), + "error": pr_result.get("error") + } + except Exception as e: + return { + "success": False, + "error": f"Failed to push changes and create PR: {str(e)}", + "pr_url": None + } def cleanup_directory(self, directory: str): """Clean up a workspace directory.""" diff --git a/src/agents/cve_solver.py b/src/agents/cve_solver.py index 50c8430..db55e36 100644 --- a/src/agents/cve_solver.py +++ b/src/agents/cve_solver.py @@ -50,22 +50,19 @@ def solve_vulnerability(self, vulnerability: Dict[str, Any], repo_url: str, port if activity_callback: activity_callback("Setting up workspace") - # Clone repository to workspace + # Create agent with default tools and secrets if activity_callback: - activity_callback("Cloning repository") + activity_callback("Initializing AI agent with secrets") - if not self.clone_repository(repo_url, repo_dir): - result["error"] = "Failed to clone repository" - return result + agent, conversation = self.create_agent_conversation() - # Create agent with default tools + # Clone repository to workspace using conversation (for secret injection) if activity_callback: - activity_callback("Initializing AI agent") - - agent, conversation = self.create_agent_conversation() + activity_callback("Cloning repository with authentication") - # Set up environment variables for the agent - self.setup_git_environment() + if not self.clone_repository(repo_url, repo_dir, conversation): + result["error"] = "Failed to clone repository" + return result # Create fix instructions fix_message = self.prompt_manager.get_solver_main_prompt( @@ -88,7 +85,7 @@ def solve_vulnerability(self, vulnerability: Dict[str, Any], repo_url: str, port result = self._get_fix_summary(conversation, solver_dir, result, activity_callback) # Final step - ensure PR creation - self._finalize_pull_request(conversation, vulnerability, activity_callback) + self._finalize_pull_request(conversation, vulnerability, activity_callback, repo_url, repo_dir) except Exception as e: result["error"] = str(e) @@ -147,11 +144,78 @@ def _get_fix_summary(self, conversation, solver_dir: str, result: Dict[str, Any] return result - def _finalize_pull_request(self, conversation, vulnerability: Dict[str, Any], activity_callback): - """Ensure pull request is created.""" + def _finalize_pull_request(self, conversation, vulnerability: Dict[str, Any], activity_callback, repo_url: str = None, repo_dir: str = None): + """Create pull request using GitHub API.""" if activity_callback: - activity_callback("Finalizing pull request") - pr_message = self.prompt_manager.get_solver_pr_finalize_prompt(vulnerability=vulnerability) - conversation.send_message(pr_message) + activity_callback("Creating pull request via GitHub API") + + if not repo_url or not repo_dir: + # Fallback to old method if repo info not available + pr_message = self.prompt_manager.get_solver_pr_finalize_prompt(vulnerability=vulnerability) + conversation.send_message(pr_message) + self._run_with_activity_updates(conversation, activity_callback) + return - self._run_with_activity_updates(conversation, activity_callback) \ No newline at end of file + try: + # Extract repo owner and name from URL + repo_parts = repo_url.replace('https://github.com/', '').replace('.git', '').split('/') + if len(repo_parts) >= 2: + repo_owner, repo_name = repo_parts[0], repo_parts[1] + + # Create branch name + branch_name = f"fix/cve-{vulnerability.get('id', 'unknown')}" + + # Create and push branch with fixes + if activity_callback: + activity_callback("Creating feature branch and pushing changes") + + # Create branch + self.create_github_branch(conversation, repo_dir, branch_name) + + # Push changes and create PR + pr_result = self.push_changes_and_create_pr( + conversation=conversation, + repo_dir=repo_dir, + repo_owner=repo_owner, + repo_name=repo_name, + branch_name=branch_name, + commit_message=f"Fix {vulnerability.get('id', 'CVE')}: {vulnerability.get('title', 'Security vulnerability')}", + pr_title=f"Security Fix: {vulnerability.get('title', 'CVE Vulnerability')}", + pr_body=f"""## Summary +This PR fixes the security vulnerability {vulnerability.get('id', 'identified by our scanner')}. + +## Vulnerability Details +- **CVE ID**: {vulnerability.get('id', 'N/A')} +- **Severity**: {vulnerability.get('severity', 'Unknown')} +- **Description**: {vulnerability.get('description', 'Security vulnerability detected')} + +## Changes Made +The OpenHands agent has automatically analyzed and applied the necessary security fixes. + +## Testing +Please review the changes and run your security tests to verify the fix. + +--- +*This PR was created automatically by OpenHands CVE Solver Agent* +""" + ) + + if pr_result.get("success"): + if activity_callback: + activity_callback(f"Pull request created: {pr_result.get('pr_url')}") + else: + if activity_callback: + activity_callback(f"Failed to create PR: {pr_result.get('error')}") + else: + # Fallback to old method + pr_message = self.prompt_manager.get_solver_pr_finalize_prompt(vulnerability=vulnerability) + conversation.send_message(pr_message) + self._run_with_activity_updates(conversation, activity_callback) + + except Exception as e: + if activity_callback: + activity_callback(f"PR creation failed: {str(e)}") + # Fallback to old method + pr_message = self.prompt_manager.get_solver_pr_finalize_prompt(vulnerability=vulnerability) + conversation.send_message(pr_message) + self._run_with_activity_updates(conversation, activity_callback) \ No newline at end of file From 3a956ed783163e2de44c5a4583cf18f69b5a1cc4 Mon Sep 17 00:00:00 2001 From: openhands Date: Wed, 19 Nov 2025 13:48:22 +0000 Subject: [PATCH 2/5] Fix StaticSecret serialization issue - Remove StaticSecret storage in instance variables to avoid JSON serialization errors - Create secrets dynamically in _create_secrets() method when needed - Update create_agent_conversation() to use dynamic secrets creation - Maintain same functionality while fixing serialization compatibility Co-authored-by: openhands --- src/agents/base_agent.py | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/src/agents/base_agent.py b/src/agents/base_agent.py index a15b4b0..0fe869b 100644 --- a/src/agents/base_agent.py +++ b/src/agents/base_agent.py @@ -29,19 +29,21 @@ def __init__(self, config: Config, agent_id: str, workspace: RemoteWorkspace): base_url=config.llm_base_url, ) - # Set up secrets for the conversation - self.secrets = {} - self._setup_secrets() + # Note: Secrets are created dynamically to avoid serialization issues - def _setup_secrets(self): - """Set up secrets that will be available to the conversation.""" + def _create_secrets(self) -> Dict[str, Any]: + """Create secrets dictionary for conversation use.""" + secrets = {} + # Add GITHUB_TOKEN if available github_token = os.getenv('GITHUB_TOKEN') if github_token: - self.secrets['GITHUB_TOKEN'] = StaticSecret(value=SecretStr(github_token)) + secrets['GITHUB_TOKEN'] = StaticSecret(value=SecretStr(github_token)) # Add other common secrets as needed # You can add more secrets here following the same pattern + + return secrets def create_workspace_directory(self, prefix: str) -> str: """Create a unique workspace directory.""" @@ -95,16 +97,19 @@ def create_agent_conversation(self) -> tuple[Agent, Conversation]: tools=get_default_tools(enable_browser=False), ) + # Create secrets dynamically to avoid serialization issues + secrets = self._create_secrets() + conversation = Conversation( agent=agent, workspace=self.workspace, visualizer=None, - secrets=self.secrets, # Pass secrets to the conversation + secrets=secrets, # Pass secrets to the conversation ) # Update secrets in the conversation (this ensures they're properly registered) - if self.secrets: - conversation.update_secrets(self.secrets) + if secrets: + conversation.update_secrets(secrets) return agent, conversation From 5cde4fcfac3d2985d30bc39061741641cce2404f Mon Sep 17 00:00:00 2001 From: openhands Date: Wed, 19 Nov 2025 13:48:29 +0000 Subject: [PATCH 3/5] Remove documentation file Co-authored-by: openhands --- docs/SECRETS_AND_GITHUB_API.md | 324 --------------------------------- 1 file changed, 324 deletions(-) delete mode 100644 docs/SECRETS_AND_GITHUB_API.md diff --git a/docs/SECRETS_AND_GITHUB_API.md b/docs/SECRETS_AND_GITHUB_API.md deleted file mode 100644 index b35b3df..0000000 --- a/docs/SECRETS_AND_GITHUB_API.md +++ /dev/null @@ -1,324 +0,0 @@ -# OpenHands Secrets Manager & GitHub API Integration - -This document explains how to properly use the OpenHands secrets manager to handle sensitive data like `GITHUB_TOKEN` and how to use the GitHub API to create Pull Requests programmatically. - -## Table of Contents - -1. [Overview](#overview) -2. [Setting up the Secrets Manager](#setting-up-the-secrets-manager) -3. [Using Secrets in Conversations](#using-secrets-in-conversations) -4. [GitHub API Integration](#github-api-integration) -5. [Complete Workflow Example](#complete-workflow-example) -6. [Best Practices](#best-practices) -7. [Troubleshooting](#troubleshooting) - -## Overview - -The OpenHands SDK provides a robust secrets management system that: -- Securely stores sensitive values like API tokens -- Automatically injects secrets into bash commands -- Masks secret values in outputs and logs -- Supports both static secrets and dynamic secret sources - -## Setting up the Secrets Manager - -### 1. Import Required Components - -```python -from openhands.sdk.conversation.secret_source import StaticSecret -from pydantic import SecretStr -``` - -### 2. Create Secret Sources - -```python -def _setup_secrets(self) -> dict: - """Set up secrets for the conversation.""" - secrets = {} - - # Add GITHUB_TOKEN if available - github_token = os.getenv('GITHUB_TOKEN') - if github_token: - secrets['GITHUB_TOKEN'] = StaticSecret(value=SecretStr(github_token)) - - # Add other secrets as needed - api_key = os.getenv('API_KEY') - if api_key: - secrets['API_KEY'] = StaticSecret(value=SecretStr(api_key)) - - return secrets -``` - -### 3. Configure Conversation with Secrets - -```python -def create_agent_conversation(self) -> tuple[Agent, Conversation]: - """Create an agent and conversation instance with secrets.""" - agent = Agent( - llm=self.llm, - tools=get_default_tools(enable_browser=False), - ) - - conversation = Conversation( - agent=agent, - workspace=self.workspace, - visualizer=None, - secrets=self.secrets, # Pass secrets during creation - ) - - # Update secrets in the conversation (ensures proper registration) - if self.secrets: - conversation.update_secrets(self.secrets) - - return agent, conversation -``` - -## Using Secrets in Conversations - -### Automatic Secret Injection - -When you use a secret name in a bash command, the secrets manager automatically injects the actual value: - -```python -# This command will have $GITHUB_TOKEN automatically replaced -conversation.send_message(""" -git clone https://$GITHUB_TOKEN@github.com/owner/repo.git -""") -conversation.run() -``` - -### Secret Masking - -All secret values are automatically masked in outputs: -- Console logs show `` instead of actual values -- Terminal outputs are scrubbed of secret values -- Error messages don't expose secrets - -## GitHub API Integration - -### 1. Creating Pull Requests - -Use the GitHub API instead of browser automation for creating PRs: - -```python -def create_github_pr(self, repo_owner: str, repo_name: str, title: str, - body: str, head_branch: str, base_branch: str = "main") -> Dict[str, Any]: - """Create a GitHub Pull Request using the GitHub API.""" - github_token = os.getenv('GITHUB_TOKEN') - if not github_token: - return {"success": False, "error": "GITHUB_TOKEN not available"} - - url = f"https://api.github.com/repos/{repo_owner}/{repo_name}/pulls" - headers = { - "Authorization": f"token {github_token}", - "Accept": "application/vnd.github.v3+json", - "Content-Type": "application/json" - } - - data = { - "title": title, - "body": body, - "head": head_branch, - "base": base_branch - } - - try: - response = requests.post(url, headers=headers, json=data) - response.raise_for_status() - - pr_data = response.json() - return { - "success": True, - "pr_url": pr_data.get("html_url"), - "pr_number": pr_data.get("number"), - "pr_data": pr_data - } - except requests.exceptions.RequestException as e: - return {"success": False, "error": f"Failed to create PR: {str(e)}"} -``` - -### 2. Complete Git Workflow with Secrets - -```python -def push_changes_and_create_pr(self, conversation: Conversation, repo_dir: str, - repo_owner: str, repo_name: str, branch_name: str, - commit_message: str, pr_title: str, pr_body: str) -> Dict[str, Any]: - """Push changes to GitHub and create a Pull Request.""" - try: - # Stage, commit, and push changes (secrets automatically injected) - conversation.send_message(f""" -cd {repo_dir} -git add . -git commit -m "{commit_message}" -git push -u origin {branch_name} -""") - conversation.run() - - # Create PR using GitHub API - pr_result = self.create_github_pr( - repo_owner=repo_owner, - repo_name=repo_name, - title=pr_title, - body=pr_body, - head_branch=branch_name - ) - - return pr_result - except Exception as e: - return {"success": False, "error": f"Failed to push and create PR: {str(e)}"} -``` - -## Complete Workflow Example - -Here's a complete example of how a CVE solver agent would use secrets and GitHub API: - -```python -class CVESolver(BaseAgent): - def solve_vulnerability(self, vulnerability: Dict[str, Any], repo_url: str) -> Dict[str, Any]: - """Fix a vulnerability and create a PR.""" - - # 1. Create conversation with secrets - agent, conversation = self.create_agent_conversation() - - # 2. Clone repository (secrets automatically injected) - repo_dir = "/tmp/cve_fix" - success = self.clone_repository(repo_url, repo_dir, conversation) - if not success: - return {"success": False, "error": "Failed to clone repository"} - - # 3. Create feature branch - branch_name = f"fix/cve-{vulnerability['id']}" - self.create_github_branch(conversation, repo_dir, branch_name) - - # 4. Apply fixes using the agent - conversation.send_message(f""" -Please analyze and fix the vulnerability: {vulnerability['description']} -The vulnerable file is: {vulnerability['file']} -Apply the necessary security fixes. -""") - conversation.run() - - # 5. Push changes and create PR - repo_parts = repo_url.replace('https://github.com/', '').replace('.git', '').split('/') - repo_owner, repo_name = repo_parts[0], repo_parts[1] - - pr_result = self.push_changes_and_create_pr( - conversation=conversation, - repo_dir=repo_dir, - repo_owner=repo_owner, - repo_name=repo_name, - branch_name=branch_name, - commit_message=f"Fix {vulnerability['id']}: {vulnerability['title']}", - pr_title=f"Security Fix: {vulnerability['title']}", - pr_body=f"""## Summary -This PR fixes the security vulnerability {vulnerability['id']}. - -## Vulnerability Details -- **CVE ID**: {vulnerability['id']} -- **Severity**: {vulnerability['severity']} -- **Description**: {vulnerability['description']} - -## Changes Made -The agent has automatically applied the necessary security fixes. - -## Testing -Please review the changes and run your security tests to verify the fix. -""" - ) - - return pr_result -``` - -## Best Practices - -### 1. Secret Management -- Always use `StaticSecret` to wrap sensitive values -- Never log or print secret values directly -- Use environment variables to provide secrets to the application -- Register secrets with conversations using `update_secrets()` - -### 2. GitHub API Usage -- Use the GitHub API instead of browser automation -- Handle API rate limits and errors gracefully -- Include meaningful PR titles and descriptions -- Use proper branch naming conventions - -### 3. Error Handling -- Always check if secrets are available before using them -- Handle API failures gracefully -- Provide meaningful error messages -- Clean up resources (directories, branches) on failure - -### 4. Security -- Never expose secrets in logs or error messages -- Use HTTPS for all GitHub operations -- Validate repository URLs before cloning -- Use minimal required permissions for tokens - -## Troubleshooting - -### Common Issues - -1. **GITHUB_TOKEN not found** - ``` - Error: GITHUB_TOKEN not available - ``` - **Solution**: Set the `GITHUB_TOKEN` environment variable with a valid GitHub personal access token. - -2. **Secret not injected in commands** - ``` - Error: $GITHUB_TOKEN not replaced in git command - ``` - **Solution**: Ensure secrets are registered with the conversation using `update_secrets()`. - -3. **GitHub API authentication failed** - ``` - Error: 401 Unauthorized - ``` - **Solution**: Verify the GitHub token has the required permissions (repo access). - -4. **PR creation failed** - ``` - Error: 422 Unprocessable Entity - ``` - **Solution**: Check that the branch exists and the PR doesn't already exist. - -### Debugging Tips - -1. **Check secret registration**: - ```python - print(f"Secrets registered: {list(conversation.state.secret_registry.secret_sources.keys())}") - ``` - -2. **Verify token permissions**: - ```bash - curl -H "Authorization: token $GITHUB_TOKEN" https://api.github.com/user - ``` - -3. **Test API connectivity**: - ```python - response = requests.get("https://api.github.com/rate_limit", - headers={"Authorization": f"token {github_token}"}) - print(response.json()) - ``` - -## Running the Demo - -To see the secrets manager and GitHub API in action, run the demonstration script: - -```bash -cd /workspace/project/cve-demo -python examples/secrets_and_github_api_demo.py -``` - -This will show you: -- How secrets are configured and registered -- How automatic secret injection works -- How to use the GitHub API for PR creation -- A complete CVE fix workflow example - -## Additional Resources - -- [OpenHands SDK Documentation](https://github.com/openhands/software-agent-sdk) -- [GitHub API Documentation](https://docs.github.com/en/rest) -- [GitHub Personal Access Tokens](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token) \ No newline at end of file From e71d803b68b43b9debc37bf2f547bc724939419a Mon Sep 17 00:00:00 2001 From: openhands Date: Wed, 19 Nov 2025 13:51:11 +0000 Subject: [PATCH 4/5] Fix remaining self.secrets reference - Replace self.secrets check with os.getenv('GITHUB_TOKEN') in clone_repository method - Ensures no remaining references to removed secrets instance variable - Maintains same functionality for GitHub token detection Co-authored-by: openhands --- src/agents/base_agent.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/agents/base_agent.py b/src/agents/base_agent.py index 0fe869b..b348b82 100644 --- a/src/agents/base_agent.py +++ b/src/agents/base_agent.py @@ -62,7 +62,7 @@ def clone_repository(self, repo_url: str, target_dir: str, conversation: Optiona Returns: True if clone was successful, False otherwise """ - if 'github.com' in repo_url and 'GITHUB_TOKEN' in self.secrets: + if 'github.com' in repo_url and os.getenv('GITHUB_TOKEN'): # Use authenticated clone with token from secrets manager if repo_url.startswith('https://github.com/'): # The GITHUB_TOKEN will be automatically injected by the secrets manager From 406d6ccd081002bcd85dfc8234a10fd5e234b727 Mon Sep 17 00:00:00 2001 From: openhands Date: Wed, 19 Nov 2025 13:53:54 +0000 Subject: [PATCH 5/5] Simplify secrets handling to avoid serialization issues - Use string values directly instead of StaticSecret objects - Remove StaticSecret import as it's no longer needed - Follow the pattern from software-agent-sdk examples - Maintains same functionality with automatic secret injection Co-authored-by: openhands --- src/agents/base_agent.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/agents/base_agent.py b/src/agents/base_agent.py index b348b82..e206daa 100644 --- a/src/agents/base_agent.py +++ b/src/agents/base_agent.py @@ -7,7 +7,7 @@ from typing import Dict, Any, Optional from pydantic import SecretStr from openhands.sdk import LLM, Agent, Conversation, RemoteWorkspace -from openhands.sdk.conversation.secret_source import StaticSecret + from openhands.tools.preset.default import get_default_tools from config.config import Config @@ -38,7 +38,7 @@ def _create_secrets(self) -> Dict[str, Any]: # Add GITHUB_TOKEN if available github_token = os.getenv('GITHUB_TOKEN') if github_token: - secrets['GITHUB_TOKEN'] = StaticSecret(value=SecretStr(github_token)) + secrets['GITHUB_TOKEN'] = github_token # Pass as string directly # Add other common secrets as needed # You can add more secrets here following the same pattern