# Credential Security Standards

**CRITICAL:** Credentials are sensitive data that must be handled with extreme care. Violations of these standards are security incidents.

## Core Principles

1. **Never log credentials** - Not even partially or obfuscated
2. **Use environment variables exclusively** - Never hardcode or pass in prompts
3. **Validate without exposing** - Check format without revealing content
4. **Fail securely** - Missing credentials should halt execution
5. **Clean up after use** - Remove any temporary credential storage

## Required Credentials

```python
REQUIRED_CREDENTIALS = {
    # === Primary Bot Account (implementation, issues, PRs, merges) ===
    'FORGEJO_PAT': {
        'description': 'Forgejo Personal Access Token (primary bot account)',
        'format': r'^[a-f0-9]{40}$',  # 40 hex characters
        'usage': 'HTTPS git authentication and API access for all non-review operations'
    },
    'GIT_USER_NAME': {
        'description': 'Git commit author name',
        'format': r'^.+$',  # Non-empty
        'usage': 'Git commit attribution'
    },
    'GIT_USER_EMAIL': {
        'description': 'Git commit author email',
        'format': r'^[^@]+@[^@]+\.[^@]+$',  # Basic email format
        'usage': 'Git commit attribution'
    },
    'FORGEJO_USERNAME': {
        'description': 'Forgejo username (primary bot account)',
        'format': r'^[a-zA-Z0-9_-]+$',  # Alphanumeric with _ and -
        'usage': 'Primary bot identity for PR creation, issue management, and ownership detection'
    },
    'FORGEJO_PASSWORD': {
        'description': 'Forgejo password (primary bot account)',
        'format': r'^.{8,}$',  # At least 8 characters
        'usage': 'Web UI access for CI logs when API unavailable'
    },

    # === Reviewer Bot Account (reviews only — separate Forgejo user) ===
    'FORGEJO_REVIEWER_PAT': {
        'description': 'Forgejo Personal Access Token (reviewer bot account)',
        'format': r'^[a-f0-9]{40}$',  # 40 hex characters
        'usage': 'API authentication for PR reviews ONLY. This is a DIFFERENT Forgejo account that can formally approve PRs created by the primary bot.'
    },
    'FORGEJO_REVIEWER_USERNAME': {
        'description': 'Forgejo username (reviewer bot account)',
        'format': r'^[a-zA-Z0-9_-]+$',  # Alphanumeric with _ and -
        'usage': 'Reviewer identity — MUST be a different user than FORGEJO_USERNAME to allow formal APPROVED reviews on bot-created PRs'
    },
    'FORGEJO_REVIEWER_PASSWORD': {
        'description': 'Forgejo password (reviewer bot account)',
        'format': r'^.{8,}$',  # At least 8 characters
        'usage': 'Web UI access for CI logs via ci-log-fetcher when invoked by the PR Reviewer'
    },
}

# NOTE: The dual-account architecture (spec Section 6.17) uses two Forgejo accounts:
# - Primary account (FORGEJO_*): creates PRs, issues, pushes code, merges
# - Reviewer account (FORGEJO_REVIEWER_*): reviews PRs, posts review comments
# Since they are different Forgejo users, formal APPROVED reviews work natively.

OPTIONAL_CREDENTIALS = {
    'CA_MAX_PARALLEL_WORKERS': {
        'description': 'Maximum parallel workers',
        'format': r'^\d+$',  # Positive integer
        'default': '4',
        'usage': 'Parallelism control'
    },
    'OPENCODE_SERVER_PASSWORD': {
        'description': 'OpenCode server password',
        'format': r'^.+$',  # Non-empty if set
        'usage': 'OpenCode API authentication'
    }
}
```

## Credential Validation

```python
def validate_credentials(required_only=False):
    """
    Validate all required (and optionally optional) credentials.
    Returns (valid: bool, missing: list, malformed: list)
    
    CRITICAL: This function validates WITHOUT logging credential values.
    """
    
    missing = []
    malformed = []
    
    # Check required credentials
    for var_name, config in REQUIRED_CREDENTIALS.items():
        value = os.environ.get(var_name, '').strip()
        
        if not value:
            missing.append(var_name)
        elif config.get('format'):
            # Validate format without logging the value
            if not re.match(config['format'], value):
                malformed.append(var_name)
                # Log the issue without the value
                print(f"[CREDENTIAL ERROR] {var_name} format validation failed")
        
    # Check optional credentials if requested
    if not required_only:
        for var_name, config in OPTIONAL_CREDENTIALS.items():
            value = os.environ.get(var_name, '').strip()
            
            if value and config.get('format'):
                if not re.match(config['format'], value):
                    malformed.append(var_name)
                    print(f"[CREDENTIAL WARNING] {var_name} format validation failed")
    
    # Report results without exposing values
    if missing:
        print(f"[CREDENTIAL ERROR] Missing required credentials: {missing}")
    if malformed:
        print(f"[CREDENTIAL ERROR] Malformed credentials: {malformed}")
        
    return (not missing and not malformed, missing, malformed)


def get_credential(var_name, default=None):
    """
    Safely retrieve a credential value.
    
    NEVER use this in print statements or logs!
    """
    
    if var_name not in REQUIRED_CREDENTIALS and var_name not in OPTIONAL_CREDENTIALS:
        raise ValueError(f"Unknown credential: {var_name}")
    
    value = os.environ.get(var_name, '').strip()
    
    if not value:
        if var_name in OPTIONAL_CREDENTIALS:
            return OPTIONAL_CREDENTIALS[var_name].get('default', default)
        return None
        
    return value


def check_credential_exists(var_name):
    """Check if a credential is set without retrieving its value."""
    return bool(os.environ.get(var_name, '').strip())
```

## Safe Usage Patterns

### Git Configuration

```python
def configure_git_safely(clone_dir):
    """Configure git identity without logging credentials."""
    
    name = get_credential('GIT_USER_NAME')
    email = get_credential('GIT_USER_EMAIL')
    
    if not name or not email:
        raise ValueError("Git identity credentials not available")
    
    # Configure without echoing values
    os.chdir(clone_dir)
    subprocess.run(['git', 'config', 'user.name', name], 
                   capture_output=True, check=True)
    subprocess.run(['git', 'config', 'user.email', email], 
                   capture_output=True, check=True)
    
    print("[GIT] Configured user identity")  # Note: no values logged
```

### HTTPS Authentication

```python
def get_authenticated_url(base_url):
    """Build authenticated URL without exposing the token."""
    
    pat = get_credential('FORGEJO_PAT')
    if not pat:
        raise ValueError("FORGEJO_PAT not available")
    
    # Parse URL
    parsed = urllib.parse.urlparse(base_url)
    
    # Insert token (never log this URL!)
    auth_url = f"{parsed.scheme}://{pat}@{parsed.netloc}{parsed.path}"
    
    return auth_url


def clone_with_auth(repo_url, target_dir):
    """Clone repository with authentication."""
    
    auth_url = get_authenticated_url(repo_url)
    
    # Clone without echoing the URL
    result = subprocess.run(
        ['git', 'clone', auth_url, target_dir],
        capture_output=True,
        text=True
    )
    
    if result.returncode == 0:
        print(f"[GIT] Successfully cloned to {target_dir}")
    else:
        # Log error without exposing URL
        print(f"[GIT] Clone failed: {result.stderr}")
        # Clean up any partial clone
        if os.path.exists(target_dir):
            shutil.rmtree(target_dir)
        raise RuntimeError("Git clone failed")
```

### Web Authentication

```python
def web_login_safely():
    """Login to Forgejo web UI without exposing credentials."""
    
    username = get_credential('FORGEJO_USERNAME')
    password = get_credential('FORGEJO_PASSWORD') 
    
    if not username or not password:
        raise ValueError("Web login credentials not available")
    
    cookie_file = f"/tmp/cookies_{os.getpid()}.txt"
    
    try:
        # Get CSRF token
        csrf_result = subprocess.run(
            ['curl', '-s', '-c', cookie_file, 'https://git.cleverthis.com/user/login'],
            capture_output=True, text=True
        )
        
        csrf_match = re.search(r'name="_csrf" value="([^"]+)"', csrf_result.stdout)
        if not csrf_match:
            raise RuntimeError("Could not get CSRF token")
        
        csrf_token = csrf_match.group(1)
        
        # Login (credentials in POST data, not logged)
        login_result = subprocess.run([
            'curl', '-s', '-b', cookie_file, '-c', cookie_file,
            '-X', 'POST', 'https://git.cleverthis.com/user/login',
            '-d', f'user_name={username}',
            '-d', f'password={password}',
            '-d', f'_csrf={csrf_token}',
            '-L'
        ], capture_output=True)
        
        # Check login success without exposing response
        if 'user/settings' in login_result.stdout.decode('utf-8', errors='ignore'):
            print("[AUTH] Web login successful")
            return cookie_file
        else:
            print("[AUTH] Web login failed") 
            raise RuntimeError("Web authentication failed")
            
    except Exception as e:
        # Clean up on any error
        if os.path.exists(cookie_file):
            os.remove(cookie_file)
        raise
```

## Startup Validation Template

Every agent MUST validate credentials at startup:

```python
def agent_startup():
    """Standard agent startup with credential validation."""
    
    print("[STARTUP] Validating credentials...")
    
    # Validate all required credentials
    valid, missing, malformed = validate_credentials(required_only=True)
    
    if not valid:
        error_msg = "Cannot start: credential validation failed\n"
        if missing:
            error_msg += f"Missing: {', '.join(missing)}\n"
        if malformed:
            error_msg += f"Malformed: {', '.join(malformed)}\n"
            
        print(f"[FATAL] {error_msg}")
        
        # Provide helpful information without exposing values
        print("\nRequired environment variables:")
        for var, config in REQUIRED_CREDENTIALS.items():
            status = "✓ Set" if check_credential_exists(var) else "✗ Missing"
            print(f"  {var}: {status} - {config['description']}")
            
        raise ValueError("Credential validation failed")
    
    print("[STARTUP] All credentials validated successfully")
    
    # Continue with agent initialization...
```

## Security Rules

1. **NO credential logging** - Never print, log, or echo credential values
2. **NO credentials in prompts** - All credentials via environment only
3. **NO credentials in comments** - Don't post credentials to Forgejo
4. **NO credentials in temp files** - Except cookies, which must be cleaned up
5. **NO partial credentials** - Don't log even parts of tokens
6. **NO credential defaults** - Required credentials have no defaults
7. **Validate early** - Check credentials at startup, not during work

## Common Mistakes to Avoid

```python
# ❌ WRONG - Logs the token
print(f"Using PAT: {os.environ['FORGEJO_PAT']}")

# ✓ CORRECT - Confirms presence without exposing
print("FORGEJO_PAT is configured" if check_credential_exists('FORGEJO_PAT') else "FORGEJO_PAT missing")

# ❌ WRONG - Includes token in URL that might be logged
clone_url = f"https://{pat}@git.cleverthis.com/repo.git"
print(f"Cloning from {clone_url}")

# ✓ CORRECT - Never logs authenticated URLs
auth_url = get_authenticated_url("https://git.cleverthis.com/repo.git")
subprocess.run(['git', 'clone', auth_url, target], capture_output=True)
print(f"Cloning repository to {target}")

# ❌ WRONG - Credential in error message
try:
    api_call(token)
except:
    print(f"API call failed with token {token}")

# ✓ CORRECT - Generic error without credential
try:
    api_call(token)
except:
    print("API authentication failed")
```

## Enforcement

The system-watchdog will monitor for:
- Credentials appearing in Forgejo comments
- Credentials in log output
- Git commits containing credentials  
- Temp files with credentials not cleaned up
- Agents starting without validation