Files
headquarter/docs/development.md
T
alex e071e3418d docs(FN-007): add repository connection documentation
- Update development.md with Repository Connections section
- Document SSH key generation and provider adapter usage
2026-05-16 13:43:00 +02:00

13 KiB

Development Guide

Prerequisites

  • Node.js ≥ 20 and pnpm ≥ 9
  • Python ≥ 3.11 with venv support
  • Docker and Docker Compose (for local services)

Installation

# Install Node dependencies and Python virtualenv + packages
make install

# Or manually:
pnpm install
cd apps/api && python3 -m venv .venv && .venv/bin/pip install -e ".[dev]"

Environment Setup

Copy the root environment example and fill in local values:

cp .env.example .env

Copy the frontend environment example:

cp apps/web/.env.example apps/web/.env

Frontend Authentication (OIDC)

The frontend uses OpenID Connect (OIDC) with PKCE for authentication. Configure the following environment variables in apps/web/.env:

Variable Description Example
VITE_API_URL Backend API base URL http://localhost:8000
VITE_OIDC_ISSUER OIDC provider issuer URL https://authentik.example.com/application/o/headquarter
VITE_OIDC_CLIENT_ID OIDC client ID headquarter-web
VITE_OIDC_REDIRECT_URI Post-login redirect URL http://localhost:5173/callback

Authentication Flow:

  1. User clicks login → redirected to OIDC provider authorize endpoint
  2. User authenticates with provider
  3. Provider redirects to /callback with authorization code
  4. Frontend exchanges code for access token (PKCE)
  5. Token stored in localStorage, user info fetched from /api/v1/users/me

Logout:

  • Clears local token
  • Redirects to login page
  • User can re-authenticate via OIDC flow

Running Locally

Frontend only

cd apps/web
pnpm dev        # Vite dev server on http://localhost:5173

Backend only

cd apps/api
.venv/bin/uvicorn app.main:app --reload --port 8000

Both (via root script)

pnpm dev        # Runs frontend and backend in parallel

With Docker Compose

docker compose up --build -d

Alembic Migrations

Generate a new migration after modifying models:

cd apps/api
.venv/bin/alembic revision --autogenerate -m "description"

Apply migrations:

cd apps/api
.venv/bin/alembic upgrade head

Downgrade one revision:

cd apps/api
.venv/bin/alembic downgrade -1

Or use the Makefile targets:

cd apps/api
make revision msg="description"
make upgrade
make downgrade

Testing

Frontend

pnpm --filter @headquarter/web test

Uses Vitest + Testing Library + jsdom.

Backend

pnpm --filter @headquarter/api test

Or directly with pytest:

cd apps/api && .venv/bin/pytest

All tests

make test
# or
pnpm test

CI / Testing

A GitHub Actions workflow (.github/workflows/ci.yml) runs on every push and pull request to main. It executes two jobs in parallel:

  • web-ci — checks out the repo, installs Node.js ≥ 20 and pnpm, then runs lint, typecheck, and test for @headquarter/web.
  • api-ci — checks out the repo, sets up Python 3.11, installs API dev dependencies (pytest, ruff, mypy, httpx), starts a PostgreSQL service container, then runs ruff check, mypy, and pytest for the API.

The workflow reports pass/fail status directly on pull requests as required status checks. All commands must pass before a PR can be merged.

Linting and Type Checking

Frontend

pnpm --filter @headquarter/web lint
pnpm --filter @headquarter/web typecheck

Backend

pnpm --filter @headquarter/api lint
pnpm --filter @headquarter/api typecheck

All

make lint
make typecheck

Building

make build
# or
pnpm build

Project Layout

├── apps/
│   ├── web/          # Vite React TypeScript frontend
│   └── api/          # FastAPI Python backend
├── packages/         # Shared packages (future)
├── docs/             # Documentation
├── deploy/           # Deployment skeleton files
├── docker-compose.yml
└── package.json      # Root monorepo scripts

Conventions

  • Frontend: React functional components, TypeScript strict mode, ESLint + Ruff-like rules.
  • Backend: FastAPI, Pydantic settings, pytest, ruff, mypy.
  • Commits: Conventional commits with task ID prefix, e.g. feat(FN-002): description.

Git Abstraction

The app/git/ package in the backend provides provider-independent Git orchestration. It is split into two layers so that remote provider API logic and local CLI operations evolve independently:

Module Responsibility
types Enumerations (ProviderKind, CredentialKind, ConnectionStatus, SshKeyStatus)
provider Abstract GitProvider — remote operations (validate_connection, list_repositories, create_deploy_key, …)
credentials GitCredential / AccessTokenCredential models and CredentialStorage ABC
ssh_key SshKeyPair model and SshKeyLifecycle (Ed25519 generation via cryptography)
connection RepositoryConnection ORM mapping and ConnectionManager orchestration
operations Abstract GitOperations and concrete LocalGitOperations (subprocess-based get_status)

Security rules for the package:

  • Credential models store only encrypted_payload — no plaintext token or private_key fields.
  • SSH private keys are encrypted before storage; the field uses repr=False.
  • SSH private keys are encrypted with Fernet before storage.

Tool Spawn Workflow

The platform supports spawning development tools (e.g., code-server) as Docker containers via Docker Compose.

Architecture

  1. Tool Manifest (apps/api/app/tools/manifests/*.yml):

    • Defines Docker image, ports, volumes, environment variables, health checks
    • Loaded into in-memory registry at application startup
  2. Spawn Service (apps/api/app/services/spawn.py):

    • Generates Docker Compose service definitions from manifests
    • Handles container lifecycle: spawn, stop, status polling
    • Integrates Traefik label generation for subdomain routing
  3. API Endpoints (apps/api/app/routers/tool_instances.py):

    • POST /projects/{id}/tool-instances — Spawn a new tool instance
    • POST /projects/{id}/tool-instances/{id}/stop — Stop a running instance
    • POST /projects/{id}/tool-instances/{id}/start — Restart a stopped instance
    • GET /projects/{id}/tool-instances/{id}/status — Get container status
  4. Frontend UI:

    • /tools/spawn — Form to select tool, project, and spawn
    • /projects/{id}/instances/{id} — Instance detail with status, controls, and "Open Tool" link

Auth Proxy

Spawned tools are protected behind Traefik forwardAuth middleware:

  • Traefik forwards requests to /api/v1/auth/validate for session validation
  • code-server built-in auth is disabled (PASSWORD: "")
  • Only authenticated platform users can access spawned tools

Local Development

Ensure Docker socket is accessible and the tools network exists:

docker network create tools  # One-time setup

Spawned containers use the tools network for Traefik routing.

Config & Secrets

Overview

The platform supports scoped configuration values and encrypted secrets that are injected into tool containers at spawn time.

Scopes

Configs and secrets support four scope levels (closest match wins):

  1. Global — Available to all users and projects
  2. User — Available to a specific user across all projects
  3. Project — Available within a specific project
  4. Instance — Available to a specific tool instance

Configs

Configs are plaintext JSON values mounted as files into containers:

  • Mount path: /app/config/<key>.json
  • Permissions: 0400 (read-only, owner-only)
  • Scope resolution: instance > project > user > global

API Endpoints:

  • POST /configs — Create config
  • GET /configs — List configs (filter by scope_type, scope_id)
  • PUT /configs/{id} — Update config value
  • DELETE /configs/{id} — Delete config

Frontend:

  • /projects/{id}/configs — Config management UI

Secrets

Secrets are encrypted with Fernet and injected as environment variables:

  • Env var format: <UPPERCASE_KEY>=<decrypted_value>
  • Values are never sent to the frontend decrypted (displayed as ••••••)
  • Scope resolution: instance > project > user > global

API Endpoints:

  • POST /secrets — Create secret
  • GET /secrets — List secrets (filter by scope_type, scope_id)
  • PUT /secrets/{id} — Update secret value
  • DELETE /secrets/{id} — Delete secret

Frontend:

  • /projects/{id}/secrets — Secret management UI

Runtime Injection

When a tool instance is spawned:

  1. Configs are resolved from all applicable scopes
  2. Secrets are resolved and decrypted
  3. Config files are generated in /tmp/headquarter-configs/{instance_id}/
  4. Config files are mounted as read-only volumes
  5. Secrets are injected as environment variables
  6. Missing required secrets will fail the spawn with a clear error

Validation

Before spawning, the system validates that all required secrets exist. If any are missing, the spawn fails with an error message listing the missing secrets.

Repository Connections

Overview

The platform supports connecting Git repositories to projects with provider-independent authentication.

Architecture

  1. Repository (apps/api/app/models/repository.py):

    • Stores repository metadata (name, git_url, provider_type, default_branch)
    • Belongs to a project
  2. Repository Connection (apps/api/app/models/repository_connection.py):

    • Links a repository to a Git provider with credentials
    • Tracks connection status (pending, connected, error, disconnected)
    • Supports SSH key authentication
  3. Credential Storage (apps/api/app/git/credential_storage.py):

    • Database-backed storage for encrypted credentials
    • Uses Fernet encryption for payload
    • Supports access tokens and SSH keys
  4. Provider Adapters (apps/api/app/git/providers/):

    • GitHubAdapter and GitLabAdapter with URL parsing
    • Extensible for other providers (Gitea, Forgejo)
  5. SSH Key Lifecycle (apps/api/app/git/ssh_key.py):

    • Ed25519 key pair generation
    • Fernet-encrypted private key storage
    • Public key available for deploy key registration

API Endpoints

  • POST /projects/{id}/repositories — Add repository
  • GET /projects/{id}/repositories — List repositories
  • DELETE /projects/{id}/repositories/{id} — Remove repository
  • POST /projects/{id}/repository-connections — Create connection
  • GET /projects/{id}/repository-connections — List connections
  • DELETE /projects/{id}/repository-connections/{id} — Remove connection
  • POST /projects/{id}/repository-connections/{id}/ssh-key — Generate SSH key
  • POST /projects/{id}/repository-connections/{id}/validate — Validate connection

Frontend

  • /repositories — Repository list and creation
  • /projects/{id}/repositories/{id} — Repository detail with connections

Git Operations

Local Git operations are supported via subprocess:

  • Clone, fetch, push with credential-aware subprocess
  • Working tree status (branch, clean, untracked, modified, staged, deleted)

OpenCode Tool

Overview

OpenCode is an AI-powered terminal-based development environment accessible via web browser. It is included as a built-in tool manifest alongside code-server.

Manifest

File: apps/api/app/tools/manifests/opencode.yml

id: opencode
name: OpenCode
image: ghcr.io/opencode-ai/opencode:latest
ports:
  - container_port: 3000
    primary: true

Web Terminal Access

OpenCode exposes a terminal interface on port 3000:

  • Subdomain: opencode-{project}-{user}.{domain}
  • Health Check: GET / on port 3000
  • Terminal: Full xterm-256color support with color output

Environment Variables

The following environment variables are configured for terminal support:

Variable Value Description
TERM xterm-256color Terminal type with color support
FORCE_COLOR "1" Force color output

Workspace Mount

OpenCode mounts the project workspace at /workspace for persistent file access.

Config Mount

User-specific OpenCode configuration is mounted at /root/.config/opencode.

Usage

  1. Navigate to /tools/spawn in the frontend
  2. Select "OpenCode" from the tool dropdown
  3. Choose a project
  4. Enter an instance name
  5. Click "Spawn Tool"
  6. Once running, click "Open Tool" to access the web terminal

Differences from code-server

Feature OpenCode code-server
Interface Terminal (web-based) VS Code (web-based)
Port 3000 8080
Primary Use Terminal/CLI tasks Code editing/IDE
AI Features Built-in AI assistance Extensions required

Local Testing

To test OpenCode locally without the full platform:

docker run -it --rm \
  -p 3000:3000 \
  -e TERM=xterm-256color \
  -e FORCE_COLOR=1 \
  ghcr.io/opencode-ai/opencode:latest

Then open http://localhost:3000 in your browser.