Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .cursor/hooks.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"version": 1,
"hooks": {
"stop": [
{
"command": "bash .cursor/hooks/run-strict-gates.sh",
"timeout": 3600,
"failClosed": true
}
]
}
}
65 changes: 65 additions & 0 deletions .cursor/hooks/run-strict-gates.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
#!/usr/bin/env bash
set -euo pipefail

# Read hook payload (unused for now, but consumed to avoid broken pipes)
HOOK_INPUT="$(cat || true)"
export HOOK_INPUT

REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
cd "$REPO_ROOT"
LOG_FILE="context/quality-gate-feedback.log"
mkdir -p "$(dirname "$LOG_FILE")"

CHANGED_FILES="$(git status --porcelain | awk '{print $2}')"

if [[ -z "$CHANGED_FILES" ]]; then
echo "[strict-gates] No changed files detected; skipping checks."
printf "%s | skip | no-changes\n" "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" >> "$LOG_FILE"
exit 0
fi

RUN_BACKEND=0
RUN_FRONTEND=0

while IFS= read -r file; do
[[ -z "$file" ]] && continue
[[ "$file" == backend/* ]] && RUN_BACKEND=1
[[ "$file" == frontend/* ]] && RUN_FRONTEND=1
done <<< "$CHANGED_FILES"

EXIT_CODE=0

if [[ $RUN_BACKEND -eq 1 ]]; then
echo "[strict-gates] Running backend tests: pytest -q"
if ! (cd backend && pytest -q); then
echo "[strict-gates] Backend quality gate failed."
EXIT_CODE=1
fi
fi

if [[ $RUN_FRONTEND -eq 1 ]]; then
echo "[strict-gates] Running frontend lint: npm run lint"
if ! (cd frontend && npm run lint); then
echo "[strict-gates] Frontend lint gate failed."
EXIT_CODE=1
fi

echo "[strict-gates] Running frontend build: npm run build"
if ! (cd frontend && npm run build); then
echo "[strict-gates] Frontend build gate failed."
EXIT_CODE=1
fi
fi

if [[ $RUN_BACKEND -eq 0 && $RUN_FRONTEND -eq 0 ]]; then
echo "[strict-gates] No backend/frontend source changes detected; skipping checks."
printf "%s | skip | non-app-changes\n" "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" >> "$LOG_FILE"
fi

if [[ $EXIT_CODE -eq 0 ]]; then
printf "%s | pass | backend=%s frontend=%s\n" "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" "$RUN_BACKEND" "$RUN_FRONTEND" >> "$LOG_FILE"
else
printf "%s | fail | backend=%s frontend=%s\n" "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" "$RUN_BACKEND" "$RUN_FRONTEND" >> "$LOG_FILE"
fi

exit "$EXIT_CODE"
1 change: 1 addition & 0 deletions .cursor/rules/backend-api-routers.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -35,5 +35,6 @@ async def analyze(
- Routers **must not** run raw SQLAlchemy queries, `session.execute`, or ad-hoc ORM loads—**DB access goes through repositories/services**.
- Return Pydantic schema instances directly (FastAPI serializes them).
- File uploads use `File(...)` / `UploadFile` with manual size checks before handing off to services.
- For project uploads, keep behavior consistent with current product flow: safe scan + queue analysis of detected files, and preserve explicit compose deploy gating in deploy endpoints.
- Each router file: `router = APIRouter(prefix=..., tags=[...])` at module level.
- All routers aggregated in `app/api/router.py` via `api_router.include_router(...)`.
3 changes: 2 additions & 1 deletion .cursor/rules/backend-infrastructure.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,13 @@ alwaysApply: false

- `publish_event(DomainEvent)` publishes to multiple channels based on event context.
- `subscribe(channel)` returns an `AsyncIterator[dict]` with `try/finally` cleanup.
- Channel patterns: `user:{id}:events`, `job:{id}:events`, `container:{id}:metrics`.
- Channel patterns: `user:{id}:events`, `job:{id}:events`, `container:{id}:metrics`, `user:{id}:container:{id}:metrics`.

## Deploy State in Redis

- Key: `deploy:{user_id}:{job_id}` — JSON payload with 6-hour TTL.
- Payload should include **`containers`**, aggregates **`running_count`**, **`exited_count`**, **`unhealthy_count`**, plus **`runtime_state`** when tracked (overall deploy/monitor rollup).
- Runtime state values can include: `none`, `running`, `partial`, `unhealthy`, `exited`, `failed`, `stopping`, `stopped_by_user`, `cleanup_completed`.
- Stop signal: `deploy-stop:{user_id}:{job_id}` — flag key.

## Subprocess Tools (`tools/process.py`)
Expand Down
16 changes: 13 additions & 3 deletions .cursor/rules/backend-plugins.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,19 @@ The `context` dict passed to `run()` may contain:

## Two Plugin Flavors

- **Subprocess plugins** (hadolint, dclint): Use `run_command()` from `infrastructure/tools/process.py`. Write content to a tempfile, invoke the binary, parse JSON stdout. 45-second timeout.
- **Pure Python plugins** (security_scanner, runnability, resource_estimation): Analyze content in-process with no external dependencies.
- **Subprocess plugins** (hadolint): Use `run_command()` from `infrastructure/tools/process.py`. Write content to a tempfile, invoke the binary, parse JSON stdout. 45-second timeout.
- **Pure Python plugins** (`compose_validator`, `compose_runnability`, `security_scanner`, `resource_estimation`): Analyze content in-process with no external dependencies.

## Issue Normalization

`AnalysisService._normalize_issue()` handles multiple output formats (hadolint JSON, dclint rdjson, generic dicts). Plugin findings just need to be a list of dicts — normalization happens in the service layer.
`AnalysisService._normalize_issue()` handles multiple output formats (hadolint JSON and generic dict outputs). Plugin findings just need to be a list of dicts — normalization happens in the service layer.

## Current Registry

`PLUGIN_MAP` currently includes:

- `hadolint`
- `compose_validator`
- `compose_runnability`
- `security_scanner`
- `resource_estimation`
12 changes: 7 additions & 5 deletions .cursor/rules/docker-and-devops.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ alwaysApply: true

## 6-Service Compose Stack

Stack is defined in `docker-compose.yaml` at the repo root.
Compose stack is split across `compose.yaml` (base) with overlays `compose.dev.yaml` and `compose.prod.yaml` at the repo root.

| Service | Role | Port |
|---|---|---|
Expand All @@ -26,13 +26,14 @@ Stack is defined in `docker-compose.yaml` at the repo root.

- **Single-image, dual-role backend**: Same Dockerfile produces `api` (uvicorn) and `worker` (arq CMD override).
- **Shared volume**: `uploads_data` mounted in both `api` and `worker` for uploaded file access.
- **DinD isolation**: Worker talks to `dind` sidecar via `DOCKER_HOST=tcp://dind:2375` (no TLS). Never exposes host Docker socket.
- **DinD isolation**: Worker talks to DinD over TLS (`DOCKER_HOST=tcp://docker:2376`, certs via `DOCKER_CERT_PATH`, `DOCKER_TLS_VERIFY=1`). Never exposes host Docker socket.

## Essential Commands

```bash
cp backend/.env.example backend/.env # one-time setup
docker compose up --build # start full stack
cp .env.example .env # one-time setup
mkdir -p secrets && cp secrets/postgres_password.txt.example secrets/postgres_password.txt && cp secrets/jwt_secret.txt.example secrets/jwt_secret.txt
docker compose --env-file .env -f compose.yaml -f compose.dev.yaml up --build
docker compose exec api alembic upgrade head # run DB migrations
docker compose logs -f frontend api worker # tail logs
docker compose down -v # stop + wipe volumes
Expand All @@ -41,6 +42,7 @@ docker compose down -v # stop + wipe volumes
## Configuration

Backend settings loaded by `pydantic-settings` from env vars or `backend/.env`. Key toggles:
Backend settings loaded by `pydantic-settings` from env vars/secrets. Key toggles:
- `APP_ENV=dev` enables permissive CORS (`allow_origins=["*"]`)
- `APP_ENV=prod` restricts CORS
- `JWT_SECRET_KEY` must be changed in production
- `JWT_SECRET_KEY` is loaded from secrets in Compose deployments and must be strong in production
6 changes: 6 additions & 0 deletions .cursor/rules/frontend-react-patterns.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ await research.jobs({ limit: 20, offset: 0 });

Also exported: `auth`, `project`, `ws`, `checkHealth`, shared types (`Job`, `ApiError`, etc.).

## Project upload UX assumptions

- Current project upload flow queues analysis for all detected Dockerfiles/Compose files.
- Current project upload flow communicates automatic image builds in UX copy.
- Compose runtime start/deploy remains an explicit follow-up action from results/deploy controls.

## WebSocket

- Hold socket in `useRef`.
Expand Down
11 changes: 8 additions & 3 deletions .cursor/rules/project-architecture.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ alwaysApply: true

# Project Architecture

## When to apply

- Use this file as the architectural source of truth.
- Keep workflow details in `project-workflow.mdc` and runtime/deploy constraints in `project-runtime-safety.mdc`.

## Backend Layers (Hexagonal / Modular-Monolith)

| Layer | Path | Responsibility |
Expand All @@ -25,7 +30,7 @@ alwaysApply: true

## Project Analysis Workflow

ZIP upload → **safe scan** → user **selects** Dockerfiles/Compose files → **per-file plugin runs** → merged **`ProjectAnalysisResult`** (`per_file_results`, `project_summary`, `service_mappings`, optional **`image_build_results`**). Builds and Compose **run/deploy only on explicit user action** (`build_selected_images`, deploy controls). Details: **`project-workflow.mdc`**, **`project-runtime-safety.mdc`**.
ZIP upload → **safe scan** → auto-queue analysis for all detected Dockerfiles/Compose files → **per-file plugin runs** → merged **`ProjectAnalysisResult`** (`per_file_results`, `project_summary`, `service_mappings`, optional **`image_build_results`**). Upload flow currently defaults `build_selected_images=true`; Compose **run/deploy remains explicit** via deploy controls (`run_stack`). Details: **`project-workflow.mdc`**, **`project-runtime-safety.mdc`**.

## Redis Channel Naming

Expand All @@ -43,15 +48,15 @@ Plugins live in `backend/app/plugins/`. Each extends `BasePlugin` (single `async

## Docker-in-Docker (DinD)

Worker uses `DOCKER_HOST=tcp://dind:2375` to reach an isolated DinD sidecar. All `docker-compose` subprocess calls execute inside the worker container — never the host socket.
Worker uses TLS DinD networking (`DOCKER_HOST=tcp://docker:2376`, `DOCKER_TLS_VERIFY=1`, `DOCKER_CERT_PATH=/certs/client`) to reach an isolated sidecar. All `docker-compose` subprocess calls execute inside the worker container — never the host socket.

## Research Privacy

Research endpoints and `ResearchAnalytics` UI expose **aggregated / anonymized** fields only (`anonymized_submitter`, `public_metadata`, `public_result`). See **`research-privacy.mdc`**.

## Runtime Monitoring

Workers and Docker gateway expose **metrics** and **final container state** (running, exited, unhealthy, partial). Frontend monitoring pages must distinguish states honestly—no “Live” for exited containers. See **`results-and-monitoring.mdc`** and **`project-runtime-safety.mdc`**.
Workers and Docker gateway expose **metrics** and **final container state** (`running`, `partial`, `unhealthy`, `exited`, `failed`, `stopped_by_user`, `cleanup_completed`). Frontend monitoring pages must distinguish states honestly—no "Live" for exited containers. See **`results-and-monitoring.mdc`** and **`project-runtime-safety.mdc`**.

## Frontend

Expand Down
4 changes: 2 additions & 2 deletions .cursor/rules/project-runtime-safety.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ alwaysApply: false
## Automatic execution

- User-uploaded Docker projects **must never** run automatically after upload or scan.
- **Image build** requires explicit user selection: **`build_selected_images=true`**.
- Current upload flow sets **`build_selected_images=true`** by default for project jobs; worker still gates image-build execution on this payload flag.
- **Compose deploy/run** requires an explicit user action (API flag or control that represents user intent)—no silent `up`.

## Docker / tags / context
Expand All @@ -31,5 +31,5 @@ alwaysApply: false

- **Exited** containers are a **first-class** runtime state—same weight as running/unhealthy.
- **Do not** report exited containers as **running**.
- **`deployStatus`** (or equivalent) should distinguish: **`none`**, **`running`**, **`partial`**, **`exited`**, **`unhealthy`**, **`stopping`**.
- **`deployStatus`** (or equivalent) should distinguish: **`none`**, **`running`**, **`partial`**, **`exited`**, **`failed`**, **`unhealthy`**, **`stopping`**, **`stopped_by_user`**, **`cleanup_completed`**.
- Persist final runtime detail where applicable: **exited state**, **`exit_code`**, **`last_logs`**, **`oom_killed`**, **`restart_count`**.
19 changes: 12 additions & 7 deletions .cursor/rules/project-workflow.mdc
Original file line number Diff line number Diff line change
@@ -1,23 +1,28 @@
---
description: End-to-end project ZIP workflow — scan, select, analyze, merge, optional build/run, monitoring
description: End-to-end project ZIP workflow — scan, auto-analyze detected files, auto-build images, explicit deploy/run
alwaysApply: true
---

# Project Workflow

## When to apply

- Use this file as the canonical user flow for project upload/analyze/build/deploy.
- Keep low-level runtime semantics (deploy state names, stop/failure behavior) in `project-runtime-safety.mdc`.

## Flow

1. User uploads a **ZIP** archive.
2. Backend **scans** the tree safely and reports **detected assets** (Dockerfiles, compose files, etc.).
3. User **selects** which Dockerfiles / Compose files to analyze.
4. Each **selected** file is analyzed **separately**; results are merged into a **project** job outcome.
5. **Optional**: user-selected Dockerfile **image builds** (`build_selected_images=true`).
6. **Optional**: **Compose deploy/run** only when the user explicitly triggers it.
3. Upload flow auto-queues analysis for **all detected** Dockerfiles / Compose files.
4. Each detected file is analyzed **separately**; results are merged into a **project** job outcome.
5. Upload flow currently enables **image builds by default** (`build_selected_images=true` in payload).
6. **Compose deploy/run** remains explicit: user-triggered action only (deploy API with `run_stack=true`).
7. **Runtime monitoring** for running stacks: handle **exited**, **unhealthy**, and **partial** container states as first-class UI/backend states.

## Rules

- **Do not** analyze unselected files except when an **auto** or **full-project** fallback explicitly requires it.
- **Do not** build images unless **`build_selected_images=true`** (user opt-in).
- In current product behavior, project upload analyzes all scan hits by default (no selection step in `/api/v1/project/upload`).
- In current product behavior, upload sets **`build_selected_images=true`** by default; worker still gates build execution on this flag.
- **Do not** run or deploy Compose unless the user **explicitly** triggers run/deploy.
- Preserve merged payload fields: **`per_file_results`**, **`image_build_results`**, **`service_mappings`**, **`project_summary`**—do not drop or overwrite them unintentionally on merge or re-fetch.
1 change: 1 addition & 0 deletions .cursor/rules/research-privacy.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,4 @@ Research responses and aggregates must be **privacy-safe** and **anonymized**.

- Use **`anonymized_submitter`** (or equivalent stable anonymous bucket) instead of user identity.
- Expose only **`public_metadata`** and **`public_result`** (or fields explicitly approved for research)—nothing from the forbidden list above.
- Keep `public_metadata` allow-listed (for example file extension and aggregate-safe flags), and keep `public_result` to sanitized aggregates (counts/severity/code/doc URLs) without source snippets.
4 changes: 2 additions & 2 deletions .cursor/rules/results-and-monitoring.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,6 @@ alwaysApply: false

## Monitoring

- Surface **running**, **exited**, **unhealthy**, **partial**, and **disconnected** (WS/client) states **clearly**—different copy, color, and icons; no ambiguous “active” for dead processes.
- Surface **running**, **partial**, **unhealthy**, **exited**, **failed**, **stopping**, **stopped_by_user**, **cleanup_completed**, and **disconnected** (WS/client) states **clearly**—different copy, color, and icons; no ambiguous "active" for dead processes.
- If a container **exits before metrics** exist, show an explicit message (no empty chart pretending health).
- **Do not** label exited containers **“Live”** or imply continuous metrics when the process has stopped.
- **Do not** label exited/failed/stopped containers as **"Live"** or imply continuous metrics when the process has stopped.
32 changes: 32 additions & 0 deletions .cursor/rules/rule-governance.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
---
description: Rule governance map — topic owners, scope boundaries, and de-duplication policy
alwaysApply: true
---

# Rule Governance

## Purpose

Keep rules concise and non-duplicative by assigning a single primary owner file per topic.

## Topic owners

- **Architecture owner:** `project-architecture.mdc` (system shape and cross-layer boundaries)
- **Workflow owner:** `project-workflow.mdc` (upload/analyze/build/deploy user flow)
- **Runtime safety owner:** `project-runtime-safety.mdc` (execution constraints and deploy/runtime semantics)
- **Testing owner:** `testing-conventions.mdc` (quality gates and test expectations)
- **Backend implementation owners:** `backend-*.mdc` files (router/infrastructure/plugin/python specifics)
- **Frontend implementation owners:** `frontend-*.mdc` + `results-and-monitoring.mdc`
- **Privacy owner:** `research-privacy.mdc`
- **DevOps owner:** `docker-and-devops.mdc`

## De-duplication policy

- Put canonical statements in the owner file and keep references elsewhere short.
- When two rules overlap, prefer cross-reference instead of repeating detailed guidance.
- Use "current behavior" wording when documenting code-backed behavior that may change later.

## When to apply

- Apply this governance rule whenever adding/updating rule files.
- If a rule grows broad or duplicates another rule, split or trim it before adding new guidance.
7 changes: 6 additions & 1 deletion .cursor/rules/testing-conventions.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ alwaysApply: true

# Testing Conventions

## When to apply

- Use this file as the canonical quality gate rule for backend/frontend checks.
- Keep feature-specific behavior expectations in the corresponding workflow/runtime/privacy rules.

## Backend (pytest)

- **Flat structure**: All tests in `backend/tests/`, no nested directories.
Expand Down Expand Up @@ -36,7 +41,7 @@ Prefer `monkeypatch.setattr("app.api.routers.compose.JobRepository", FakeRepo)`
When touching these areas, keep or extend tests in:

- **`test_project_scan.py`** — project ZIP scan behavior
- **`test_project_build.py`** — optional image builds, failure isolation
- **`test_project_build.py`** — project image build behavior (currently enabled by upload flow) and failure isolation
- **`test_container_lifecycle.py`** — start/stop, exit, unhealthy states
- **`test_research_privacy.py`** — research responses leak no forbidden fields

Expand Down
26 changes: 26 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
COMPOSE_PROJECT_NAME=docker-platform

# Core app configuration
APP_ENV=dev
POSTGRES_USER=postgres
POSTGRES_DB=docker_platform
API_PORT=8000
FRONTEND_PORT=3000
ADMINER_PORT=8080
VITE_API_BASE_URL=

# Optional prod-like local resource tuning
API_MEM_LIMIT=1g
API_CPU_LIMIT=1.00
WORKER_MEM_LIMIT=1g
WORKER_CPU_LIMIT=1.00
FRONTEND_MEM_LIMIT=512m
FRONTEND_CPU_LIMIT=0.50
POSTGRES_MEM_LIMIT=1g
POSTGRES_CPU_LIMIT=1.00
REDIS_MEM_LIMIT=512m
REDIS_CPU_LIMIT=0.50

# Secrets are loaded from files (do not put real values here):
# - secrets/postgres_password.txt
# - secrets/jwt_secret.txt
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ logs/
.env
.env.*
!.env.example
secrets/postgres_password.txt
secrets/jwt_secret.txt

# Python
__pycache__/
Expand Down Expand Up @@ -41,3 +43,6 @@ temp/

# Project runtime data
storage/uploads/

# Local transcript archives (Cursor history backups)
context/transcript-backups/
Loading
Loading