Contributing to Charon

Thank you for your interest in contributing to CaddyProxyManager+! This document provides guidelines and instructions for contributing to the project.

Table of Contents

Code of Conduct

This project follows a Code of Conduct that all contributors are expected to adhere to:

Getting Started

-### Prerequisites

Development Tools

Install golangci-lint for pre-commit hooks (required for Go development):

# Option 1: Homebrew (macOS/Linux)
brew install golangci-lint

# Option 2: Go install (any platform)
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest

# Option 3: Binary installation (see https://golangci-lint.run/usage/install/)
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin

Ensure $GOPATH/bin is in your PATH:

export PATH="$PATH:$(go env GOPATH)/bin"

Verify installation:

golangci-lint --version
# Should output: golangci-lint has version 1.xx.x ...

Note: Pre-commit hooks will BLOCK commits if golangci-lint finds issues. This is intentional - fix the issues before committing.

CI/CD Go Version Management

GitHub Actions workflows automatically use Go 1.25.6 via GOTOOLCHAIN: auto, which allows the setup-go action to download and use the correct Go version even if the CI environment has an older version installed. This ensures consistent builds across all workflows.

For local development, install Go 1.25.6+ from go.dev/dl.

Fork and Clone

  1. Fork the repository on GitHub
  2. Clone your fork locally:
git clone https://github.com/YOUR_USERNAME/charon.git
cd charon
  1. Add the upstream remote:
git remote add upstream https://github.com/Wikid82/charon.git

Set Up Development Environment

Backend:

cd backend
go mod download
go run ./cmd/seed/main.go  # Seed test data
go run ./cmd/api/main.go   # Start backend

Frontend:

cd frontend
npm install
npm run dev  # Start frontend dev server

Development Workflow

Branching Strategy

Branch Flow

The project uses a three-tier branching model:

development → nightly → main
   (unstable)    (testing)  (stable)

Flow details:

  1. development → nightly: Automated daily merge at 02:00 UTC
  2. nightly → main: Manual PR after validation and testing
  3. Contributors always branch from development

Why nightly?

Creating a Feature Branch

Always branch from development:

git checkout development
git pull upstream development
git checkout -b feature/your-feature-name

Note: Never branch from nightly or main. The nightly branch is managed by automation and receives daily merges from development.

Commit Message Guidelines

Follow the Conventional Commits specification:

<type>(<scope>): <subject>

<body>

<footer>

Types:

Examples:

feat(proxy-hosts): add SSL certificate upload

- Implement certificate upload endpoint
- Add UI for certificate management
- Update database schema

Closes #123
fix(import): resolve conflict detection bug

When importing Caddyfiles with multiple domains, conflicts
were not being detected properly.

Fixes #456

Keeping Your Fork Updated

git checkout development
git fetch upstream
git merge upstream/development
git push origin development

Coding Standards

Go Backend

Example:

// GetProxyHost retrieves a proxy host by UUID.
// Returns an error if the host is not found.
func GetProxyHost(uuid string) (*models.ProxyHost, error) {
    var host models.ProxyHost
    if err := db.First(&host, "uuid = ?", uuid).Error; err != nil {
        return nil, fmt.Errorf("proxy host not found: %w", err)
    }
    return &host, nil
}

TypeScript Frontend

Example:

interface ProxyHostFormProps {
  host?: ProxyHost
  onSubmit: (data: ProxyHostData) => Promise<void>
  onCancel: () => void
}

export function ProxyHostForm({ host, onSubmit, onCancel }: ProxyHostFormProps) {
  const [domain, setDomain] = useState(host?.domain ?? '')
  // ... component logic
}

CSS/Styling

Testing Guidelines

Testing Against Nightly Builds

Before submitting a PR, test your changes against the latest nightly build:

Pull latest nightly:

docker pull ghcr.io/wikid82/charon:nightly

Run your local changes against nightly:

# Start nightly container
docker run -d --name charon-nightly \
  -p 8080:8080 \
  ghcr.io/wikid82/charon:nightly

# Test your feature/fix
curl http://localhost:8080/api/v1/health

# Clean up
docker stop charon-nightly && docker rm charon-nightly

Integration testing:

If your changes affect existing features, verify compatibility:

  1. Deploy nightly build in test environment
  2. Run your modified frontend/backend against it
  3. Verify no regressions in existing functionality
  4. Document any breaking changes in your PR

Reporting nightly issues:

If you find bugs in nightly builds:

  1. Check if the issue exists in development branch
  2. Open an issue tagged with nightly label
  3. Include nightly build date or commit SHA
  4. Provide reproduction steps

Backend Tests

Write tests for all new functionality:

func TestGetProxyHost(t *testing.T) {
    // Setup
    db := setupTestDB(t)
    host := createTestHost(db)

    // Execute
    result, err := GetProxyHost(host.UUID)

    // Assert
    assert.NoError(t, err)
    assert.Equal(t, host.Domain, result.Domain)
}

Run tests:

go test ./... -v
go test -cover ./...

Frontend Tests

Write component and hook tests using Vitest and React Testing Library:

describe('ProxyHostForm', () => {
  it('renders create form with empty fields', async () => {
    render(
      <ProxyHostForm onSubmit={vi.fn()} onCancel={vi.fn()} />
    )

    await waitFor(() => {
      expect(screen.getByText('Add Proxy Host')).toBeInTheDocument()
    })
  })
})

Run tests:

npm test              # Watch mode
npm run test:coverage # Coverage report

CrowdSec Frontend Test Coverage

The CrowdSec integration has comprehensive frontend test coverage (100%) across all modules:

See QA Coverage Report for details.

Test Coverage


Testing Emergency Break Glass Protocol

When contributing changes to security modules (ACL, WAF, Cerberus, Rate Limiting, CrowdSec), you MUST test that the emergency break glass protocol still functions correctly. A broken emergency recovery system can lock administrators out of their own systems during production incidents.

Why This Matters

The emergency break glass protocol is a critical safety mechanism. If your changes break emergency access:

Always test emergency recovery before merging security-related PRs.

Quick Test Procedure

Prerequisites

# Ensure container is running
docker-compose up -d

# Set emergency token
export CHARON_EMERGENCY_TOKEN=test-emergency-token-for-e2e-32chars

Test 1: Verify Lockout Scenario

Enable security modules with restrictive settings to simulate a lockout:

# Enable ACL with restrictive whitelist (via API or database)
curl -X POST http://localhost:8080/api/v1/settings \
  -H "Content-Type: application/json" \
  -d '{"key": "security.acl.enabled", "value": "true"}'

# Enable WAF in block mode
curl -X POST http://localhost:8080/api/v1/settings \
  -H "Content-Type: application/json" \
  -d '{"key": "security.waf.enabled", "value": "true"}'

# Enable Cerberus
curl -X POST http://localhost:8080/api/v1/settings \
  -H "Content-Type: application/json" \
  -d '{"key": "feature.cerberus.enabled", "value": "true"}'

Test 2: Verify You're Locked Out

Attempt to access a protected endpoint (should fail):

# Attempt normal access
curl http://localhost:8080/api/v1/proxy-hosts

# Expected response: 403 Forbidden
# {
#   "error": "Blocked by access control list"
# }

If you're NOT blocked, investigate why security isn't working before proceeding.

Test 3: Test Emergency Token Works (Tier 1)

Use the emergency token to regain access:

# Send emergency reset request
curl -X POST http://localhost:8080/api/v1/emergency/security-reset \
  -H "X-Emergency-Token: test-emergency-token-for-e2e-32chars" \
  -H "Content-Type: application/json"

# Expected response: 200 OK
# {
#   "success": true,
#   "message": "All security modules have been disabled",
#   "disabled_modules": [
#     "feature.cerberus.enabled",
#     "security.acl.enabled",
#     "security.waf.enabled",
#     "security.rate_limit.enabled",
#     "security.crowdsec.enabled"
#   ]
# }

If this fails: Your changes broke Tier 1 emergency access. Fix before merging.

Test 4: Verify Lockout is Cleared

Confirm you can now access protected endpoints:

# Wait for settings to propagate
sleep 5

# Test normal access (should work now)
curl http://localhost:8080/api/v1/proxy-hosts

# Expected response: 200 OK
# [... list of proxy hosts ...]

Test 5: Test Emergency Server (Tier 2 - Optional)

If the emergency server is enabled (CHARON_EMERGENCY_SERVER_ENABLED=true):

# Test emergency server health
curl http://localhost:2019/health

# Expected: {"status":"ok","server":"emergency"}

# Test emergency reset via emergency server
curl -X POST http://localhost:2019/emergency/security-reset \
  -H "X-Emergency-Token: test-emergency-token-for-e2e-32chars" \
  -u admin:changeme

# Expected: {"success":true, ...}

Complete Test Script

Save this as scripts/test-emergency-access.sh:

#!/usr/bin/env bash
set -euo pipefail

GREEN='\033[0;32m'
RED='\033[0;31m'
YELLOW='\033[1;33m'
NC='\033[0m'

echo -e "${YELLOW}Testing Emergency Break Glass Protocol${NC}"
echo "========================================"
echo ""

# Configuration
BASE_URL="http://localhost:8080"
EMERGENCY_TOKEN="${CHARON_EMERGENCY_TOKEN:-test-emergency-token-for-e2e-32chars}"

# Test 1: Enable security (create lockout scenario)
echo -e "${YELLOW}Test 1: Creating lockout scenario...${NC}"
curl -s -X POST "$BASE_URL/api/v1/settings" \
  -H "Content-Type: application/json" \
  -d '{"key": "security.acl.enabled", "value": "true"}' > /dev/null

curl -s -X POST "$BASE_URL/api/v1/settings" \
  -H "Content-Type: application/json" \
  -d '{"key": "feature.cerberus.enabled", "value": "true"}' > /dev/null

sleep 2
echo -e "${GREEN}✓ Security enabled${NC}"
echo ""

# Test 2: Verify lockout
echo -e "${YELLOW}Test 2: Verifying lockout...${NC}"
RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" "$BASE_URL/api/v1/proxy-hosts")

if [ "$RESPONSE" = "403" ]; then
  echo -e "${GREEN}✓ Lockout confirmed (403 Forbidden)${NC}"
else
  echo -e "${RED}✗ Expected 403, got $RESPONSE${NC}"
  echo -e "${YELLOW}Warning: Security may not be blocking correctly${NC}"
fi
echo ""

# Test 3: Emergency token recovery
echo -e "${YELLOW}Test 3: Testing emergency token...${NC}"
RESPONSE=$(curl -s -X POST "$BASE_URL/api/v1/emergency/security-reset" \
  -H "X-Emergency-Token: $EMERGENCY_TOKEN" \
  -H "Content-Type: application/json")

if echo "$RESPONSE" | grep -q '"success":true'; then
  echo -e "${GREEN}✓ Emergency token works${NC}"
else
  echo -e "${RED}✗ Emergency token failed${NC}"
  echo "Response: $RESPONSE"
  exit 1
fi
echo ""

# Test 4: Verify access restored
echo -e "${YELLOW}Test 4: Verifying access restored...${NC}"
sleep 5

RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" "$BASE_URL/api/v1/proxy-hosts")

if [ "$RESPONSE" = "200" ]; then
  echo -e "${GREEN}✓ Access restored (200 OK)${NC}"
else
  echo -e "${RED}✗ Access not restored, got $RESPONSE${NC}"
  exit 1
fi
echo ""

# Test 5: Emergency server (if enabled)
if curl -s http://localhost:2019/health > /dev/null 2>&1; then
  echo -e "${YELLOW}Test 5: Testing emergency server...${NC}"

  RESPONSE=$(curl -s http://localhost:2019/health)
  if echo "$RESPONSE" | grep -q '"server":"emergency"'; then
    echo -e "${GREEN}✓ Emergency server responding${NC}"
  else
    echo -e "${RED}✗ Emergency server not responding correctly${NC}"
  fi
else
  echo -e "${YELLOW}Test 5: Skipped (emergency server not enabled)${NC}"
fi
echo ""

echo "========================================"
echo -e "${GREEN}All tests passed! Emergency access is functional.${NC}"

Make executable and run:

chmod +x scripts/test-emergency-access.sh
./scripts/test-emergency-access.sh

Integration Test (Go)

Add to your backend test suite:

func TestEmergencyAccessIntegration(t *testing.T) {
    // Setup test database and router
    db := setupTestDB(t)
    router := setupTestRouter(db)

    // Enable security (create lockout scenario)
    enableSecurity(t, db)

    // Test 1: Regular endpoint should be blocked
    req := httptest.NewRequest(http.MethodGET, "/api/v1/proxy-hosts", nil)
    req.RemoteAddr = "127.0.0.1:12345"
    w := httptest.NewRecorder()
    router.ServeHTTP(w, req)

    assert.Equal(t, http.StatusForbidden, w.Code, "Regular access should be blocked")

    // Test 2: Emergency endpoint should work with valid token
    req = httptest.NewRequest(http.MethodPOST, "/api/v1/emergency/security-reset", nil)
    req.Header.Set("X-Emergency-Token", "test-emergency-token-for-e2e-32chars")
    req.RemoteAddr = "127.0.0.1:12345"
    w = httptest.NewRecorder()
    router.ServeHTTP(w, req)

    assert.Equal(t, http.StatusOK, w.Code, "Emergency endpoint should work")

    var response map[string]interface{}
    err := json.Unmarshal(w.Body.Bytes(), &response)
    require.NoError(t, err)
    assert.True(t, response["success"].(bool))

    // Test 3: Regular endpoint should work after emergency reset
    time.Sleep(2 * time.Second)
    req = httptest.NewRequest(http.MethodGET, "/api/v1/proxy-hosts", nil)
    req.RemoteAddr = "127.0.0.1:12345"
    w = httptest.NewRecorder()
    router.ServeHTTP(w, req)

    assert.Equal(t, http.StatusOK, w.Code, "Access should be restored after emergency reset")
}

E2E Test (Playwright)

Add to your Playwright test suite:

import { test, expect } from '@playwright/test'

test.describe('Emergency Break Glass Protocol', () => {
  test('should recover from complete security lockout', async ({ request }) => {
    const baseURL = 'http://localhost:8080'
    const emergencyToken = 'test-emergency-token-for-e2e-32chars'

    // Step 1: Enable all security modules
    await request.post(`${baseURL}/api/v1/settings`, {
      data: { key: 'feature.cerberus.enabled', value: 'true' }
    })
    await request.post(`${baseURL}/api/v1/settings`, {
      data: { key: 'security.acl.enabled', value: 'true' }
    })

    // Wait for settings to propagate
    await new Promise(resolve => setTimeout(resolve, 2000))

    // Step 2: Verify lockout (expect 403)
    const lockedResponse = await request.get(`${baseURL}/api/v1/proxy-hosts`)
    expect(lockedResponse.status()).toBe(403)

    // Step 3: Use emergency token to recover
    const emergencyResponse = await request.post(
      `${baseURL}/api/v1/emergency/security-reset`,
      {
        headers: { 'X-Emergency-Token': emergencyToken }
      }
    )

    expect(emergencyResponse.status()).toBe(200)
    const body = await emergencyResponse.json()
    expect(body.success).toBe(true)
    expect(body.disabled_modules).toContain('security.acl.enabled')

    // Wait for settings to propagate
    await new Promise(resolve => setTimeout(resolve, 2000))

    // Step 4: Verify access restored
    const restoredResponse = await request.get(`${baseURL}/api/v1/proxy-hosts`)
    expect(restoredResponse.ok()).toBeTruthy()
  })
})

When to Run These Tests

Run emergency access tests:

Troubleshooting Test Failures

Emergency token returns 401 Unauthorized:

Emergency token returns 403 Forbidden:

Access not restored after emergency reset:

Emergency server not responding:

Related Documentation

Adding New Skills

Charon uses Agent Skills for AI-discoverable development tasks. Skills are standardized, self-documenting task definitions that can be executed by humans and AI assistants.

What is a Skill?

A skill is a combination of:

When to Create a Skill

Create a new skill when you have a:

Examples: Running tests, building artifacts, security scans, database operations, deployment tasks

Skill Creation Process

1. Plan Your Skill

Before creating, define:

2. Create Directory Structure

# Create skill directory
mkdir -p .github/skills/{skill-name}-scripts

# Skill files will be:
# .github/skills/{skill-name}.SKILL.md         # Documentation
# .github/skills/{skill-name}-scripts/run.sh    # Execution script

3. Write the SKILL.md File

Use the template structure:

---
# agentskills.io specification v1.0
name: "skill-name"
version: "1.0.0"
description: "Brief description (max 120 chars)"
author: "Charon Project"
license: "MIT"
tags:
  - "tag1"
  - "tag2"
compatibility:
  os:
    - "linux"
    - "darwin"
  shells:
    - "bash"
requirements:
  - name: "tool"
    version: ">=1.0"
    optional: false
metadata:
  category: "category-name"
  execution_time: "short|medium|long"
  risk_level: "low|medium|high"
  ci_cd_safe: true|false
---

# Skill Name

## Overview

Brief description of what this skill does.

## Prerequisites

- List required tools
- List required permissions
- List environment setup

## Usage

```bash
.github/skills/scripts/skill-runner.sh skill-name

Examples

Example 1: Basic Usage

# Description
command example

Error Handling

Related Skills


Last Updated: YYYY-MM-DD Maintained by: Charon Project Source: Original implementation or script path


#### 4. Create the Execution Script

Create `.github/skills/{skill-name}-scripts/run.sh`:

```bash
#!/usr/bin/env bash
set -euo pipefail

# Source helper scripts
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SKILLS_SCRIPTS_DIR="$(cd "${SCRIPT_DIR}/../scripts" && pwd)"

source "${SKILLS_SCRIPTS_DIR}/_logging_helpers.sh"
source "${SKILLS_SCRIPTS_DIR}/_error_handling_helpers.sh"
source "${SKILLS_SCRIPTS_DIR}/_environment_helpers.sh"

PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"

# Validate environment
log_step "ENVIRONMENT" "Validating prerequisites"
check_command_exists "required-tool" "Please install required-tool"

# Execute skill logic
log_step "EXECUTION" "Running skill"
cd "${PROJECT_ROOT}"

# Your skill implementation here
if ! your-command; then
    error_exit "Skill execution failed"
fi

log_success "Skill completed successfully"

Make it executable:

chmod +x .github/skills/{skill-name}-scripts/run.sh

5. Validate the Skill

Run the validation tool:

# Validate single skill
python3 .github/skills/scripts/validate-skills.py --single .github/skills/{skill-name}.SKILL.md

# Validate all skills
python3 .github/skills/scripts/validate-skills.py

Fix any validation errors before proceeding.

6. Test the Skill

Test execution:

# Direct execution
.github/skills/scripts/skill-runner.sh {skill-name}

# Verify output
# Check exit codes
# Confirm expected behavior

7. Add VS Code Task (Optional)

If the skill should be available in VS Code's task menu, add to .vscode/tasks.json:

{
    "label": "Category: Skill Name",
    "type": "shell",
    "command": ".github/skills/scripts/skill-runner.sh skill-name",
    "group": "test"
}

8. Update Documentation

Add your skill to .github/skills/README.md:

| [skill-name](./skill-name.SKILL.md) | category | Description | ✅ Active |

Validation Requirements

All skills must pass validation:

Best Practices

Documentation:

Scripts:

Testing:

Metadata:

Helper Scripts Reference

Charon provides helper scripts for common operations:

Logging (_logging_helpers.sh):

Error Handling (_error_handling_helpers.sh):

Environment (_environment_helpers.sh):

Resources

Pull Request Process

Before Submitting

  1. Ensure tests pass:
# Backend
go test ./...

# Frontend
npm test -- --run
  1. Check code quality:
# Go formatting
go fmt ./...

# Frontend linting
npm run lint
  1. Update documentation if needed
  2. Add tests for new functionality
  3. Rebase on latest development branch

Submitting a Pull Request

  1. Push your branch to your fork:
git push origin feature/your-feature-name
  1. Open a Pull Request on GitHub
  2. Fill out the PR template completely
  3. Link related issues using "Closes #123" or "Fixes #456"
  4. Request review from maintainers

PR Template

## Description
Brief description of changes

## Type of Change
- [ ] Bug fix
- [ ] New feature
- [ ] Breaking change
- [ ] Documentation update

## Testing
- [ ] Unit tests added/updated
- [ ] Manual testing performed
- [ ] All tests passing

## Screenshots (if applicable)
Add screenshots of UI changes

## Checklist
- [ ] Code follows style guidelines
- [ ] Self-review performed
- [ ] Comments added for complex code
- [ ] Documentation updated
- [ ] No new warnings generated

Review Process

Issue Guidelines

Reporting Bugs

Use the bug report template and include:

Feature Requests

Use the feature request template and include:

Issue Labels

Documentation

Code Documentation

Project Documentation

When adding features, update:

Recognition

Contributors will be recognized in:

Questions?

License

By contributing, you agree that your contributions will be licensed under the project's MIT License.


Thank you for contributing to CaddyProxyManager+! 🎉