535 lines
17 KiB
Markdown
535 lines
17 KiB
Markdown
# Environment Configuration Guide
|
|
|
|
Guide for collecting complete environment information and generating `harness/config/environment.json` during harness creation.
|
|
|
|
## Overview
|
|
|
|
`environment.json` is the contract between ecl-harness-engineer and harness-executor. It describes everything the executor needs to know to start the application, set up dependencies, and run verification — but it does NOT define what to verify. Verification configuration (`verify.json`) is dynamically generated by harness-executor at task runtime.
|
|
|
|
> **Key principle**: ecl-harness-engineer answers "what does this project need to run?" — harness-executor answers "what should we check after making changes?"
|
|
|
|
---
|
|
|
|
## environment.json Schema (v2.0)
|
|
|
|
```json
|
|
{
|
|
"version": "2.0",
|
|
"project": {
|
|
"name": "my-project",
|
|
"type": "web-api | cli | frontend | library | hybrid",
|
|
"language": "go | typescript | python | java | rust",
|
|
"description": "Brief project description"
|
|
},
|
|
"runtime": {
|
|
"language": "go",
|
|
"version": "1.22",
|
|
"package_manager": "go | npm | pnpm | yarn | pip | poetry | uv | maven | gradle",
|
|
"build_command": "go build ./...",
|
|
"test_command": "go test ./...",
|
|
"lint_command": "make lint-arch"
|
|
},
|
|
"startup": {
|
|
"command": "go run ./cmd/server",
|
|
"args": ["--port", "${PORT:-8080}"],
|
|
"working_dir": ".",
|
|
"env": {
|
|
"PORT": "${PORT:-8080}",
|
|
"ENV": "development",
|
|
"LOG_LEVEL": "${LOG_LEVEL:-debug}"
|
|
},
|
|
"readiness": {
|
|
"type": "http",
|
|
"config": {
|
|
"endpoint": "/health",
|
|
"port": "${PORT:-8080}",
|
|
"expected_status": 200,
|
|
"timeout_seconds": 30,
|
|
"poll_interval_ms": 500
|
|
}
|
|
}
|
|
},
|
|
"services": [
|
|
{
|
|
"name": "postgres",
|
|
"type": "database",
|
|
"required": true,
|
|
"image": "postgres:15",
|
|
"ports": ["5432:5432"],
|
|
"env": {
|
|
"POSTGRES_USER": "${DB_USER:-postgres}",
|
|
"POSTGRES_PASSWORD": "${DB_PASSWORD}",
|
|
"POSTGRES_DB": "${DB_NAME:-app}"
|
|
},
|
|
"health_check": "pg_isready -U ${DB_USER:-postgres}",
|
|
"connection": {
|
|
"url_env": "DATABASE_URL",
|
|
"default_url": "postgres://${DB_USER:-postgres}:${DB_PASSWORD}@localhost:5432/${DB_NAME:-app}?sslmode=disable"
|
|
},
|
|
"setup": {
|
|
"migration_command": "go run ./cmd/migrate up",
|
|
"seed_command": "go run ./cmd/seed"
|
|
}
|
|
}
|
|
],
|
|
"env_vars": {
|
|
"required": {
|
|
"DATABASE_URL": {
|
|
"purpose": "PostgreSQL connection string",
|
|
"sensitive": true,
|
|
"example": "postgres://user:pass@localhost:5432/dbname"
|
|
},
|
|
"JWT_SECRET": {
|
|
"purpose": "JWT token signing key",
|
|
"sensitive": true,
|
|
"test_value_ok": true,
|
|
"test_value": "test-secret-do-not-use-in-production"
|
|
}
|
|
},
|
|
"optional": {
|
|
"PORT": {
|
|
"purpose": "HTTP server port",
|
|
"default": "8080",
|
|
"sensitive": false
|
|
},
|
|
"LOG_LEVEL": {
|
|
"purpose": "Logging verbosity",
|
|
"default": "info",
|
|
"sensitive": false
|
|
}
|
|
}
|
|
},
|
|
"endpoints": {
|
|
"health": "/health",
|
|
"base_url": "http://localhost:${PORT:-8080}"
|
|
},
|
|
"scripts": {
|
|
"setup": "harness/scripts/setup-env.sh",
|
|
"start": "harness/scripts/start-server.sh",
|
|
"teardown": "harness/scripts/teardown-env.sh"
|
|
},
|
|
"_meta": {
|
|
"generated_by": "ecl-harness-engineer",
|
|
"generated_at": "2026-03-30T10:00:00Z",
|
|
"schema_version": "2.0",
|
|
"requires_user_input": ["DATABASE_URL", "JWT_SECRET"],
|
|
"todos": [
|
|
"Confirm Redis connection if caching is needed"
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Detection Strategy (4-Step Pipeline)
|
|
|
|
### Step 1: Detect Project Type and Language
|
|
|
|
```bash
|
|
# Language detection (high confidence)
|
|
test -f go.mod && echo "go"
|
|
test -f package.json && echo "typescript/javascript"
|
|
test -f pyproject.toml && echo "python"
|
|
test -f requirements.txt && echo "python"
|
|
test -f Cargo.toml && echo "rust"
|
|
test -f pom.xml && echo "java-maven"
|
|
test -f build.gradle && echo "java-gradle"
|
|
|
|
# Project type detection (medium confidence)
|
|
# Server indicators
|
|
grep -rq "http.ListenAndServe\|gin.Default\|chi.NewRouter\|echo.New" --include="*.go" . && echo "web-api"
|
|
grep -q '"express"\|"fastify"\|"koa"\|"hono"\|"nest"' package.json 2>/dev/null && echo "web-api"
|
|
grep -rq "FastAPI\|Flask\|Django" --include="*.py" . && echo "web-api"
|
|
|
|
# CLI indicators
|
|
test -d cmd/cli && echo "cli"
|
|
grep -rq "cobra\|urfave/cli" --include="*.go" . && echo "cli"
|
|
grep -q '"commander"\|"yargs"\|"oclif"' package.json 2>/dev/null && echo "cli"
|
|
|
|
# Frontend indicators
|
|
grep -q '"react"\|"vue"\|"svelte"\|"next"\|"nuxt"' package.json 2>/dev/null && echo "frontend"
|
|
|
|
# Library indicators (no entry point, exports only)
|
|
grep -q '"main"\|"bin"' package.json 2>/dev/null || echo "library"
|
|
```
|
|
|
|
### Step 2: Detect Startup Command
|
|
|
|
**Priority order** — use the first successful detection:
|
|
|
|
| Priority | Source | Command |
|
|
|----------|--------|---------|
|
|
| 1 | Existing `harness/config/environment.json` | `jq .startup.command environment.json` |
|
|
| 2 | Dockerfile CMD/ENTRYPOINT | `grep -E "^(CMD|ENTRYPOINT)" Dockerfile` |
|
|
| 3 | docker-compose.yml command | `grep "command:" docker-compose.yml` |
|
|
| 4 | Makefile targets | `grep -E "^(run|start|serve|dev):" Makefile` |
|
|
| 5 | package.json scripts | `jq '.scripts.start // .scripts.dev' package.json` |
|
|
| 6 | Go cmd/ directory | `ls cmd/*/main.go` → `go run ./cmd/<name>` |
|
|
| 7 | Python main module | `test -f main.py && echo "python main.py"` |
|
|
| 8 | **User confirmation tool** | Required if all auto-detection fails; in Codex use `request_user_input` |
|
|
|
|
```bash
|
|
# Go: Detect startup command
|
|
if test -d cmd/; then
|
|
SERVER_CMD=""
|
|
# Look for server-like directories
|
|
for dir in cmd/*/; do
|
|
name=$(basename "$dir")
|
|
if echo "$name" | grep -qiE "server|api|web|app|service"; then
|
|
SERVER_CMD="go run ./$dir"
|
|
break
|
|
fi
|
|
done
|
|
# If no server-like dir, check if only one cmd/ exists
|
|
if [ -z "$SERVER_CMD" ]; then
|
|
cmd_count=$(ls -d cmd/*/ 2>/dev/null | wc -l)
|
|
if [ "$cmd_count" -eq 1 ]; then
|
|
SERVER_CMD="go run ./$(ls -d cmd/*/)"
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# Node.js: Check package.json
|
|
if test -f package.json; then
|
|
DEV_CMD=$(jq -r '.scripts.dev // empty' package.json 2>/dev/null)
|
|
START_CMD=$(jq -r '.scripts.start // empty' package.json 2>/dev/null)
|
|
# Detect package manager
|
|
test -f pnpm-lock.yaml && PKG_MGR="pnpm"
|
|
test -f yarn.lock && PKG_MGR="yarn"
|
|
test -f bun.lockb && PKG_MGR="bun"
|
|
PKG_MGR="${PKG_MGR:-npm}"
|
|
fi
|
|
|
|
# Python: Check for framework
|
|
if grep -q "FastAPI\|Flask" requirements.txt pyproject.toml 2>/dev/null; then
|
|
# Look for uvicorn/gunicorn patterns
|
|
grep -rn "uvicorn\|gunicorn" --include="*.py" . | head -1
|
|
fi
|
|
```
|
|
|
|
### Step 3: Detect Service Dependencies
|
|
|
|
**Scan these sources for service dependencies:**
|
|
|
|
```bash
|
|
# Docker Compose (highest confidence)
|
|
if test -f docker-compose.yml; then
|
|
# Extract service names and images
|
|
grep -E "^\s+\w+:" docker-compose.yml | grep -v "version\|services"
|
|
grep "image:" docker-compose.yml
|
|
fi
|
|
|
|
# Code imports (medium confidence)
|
|
# PostgreSQL
|
|
grep -rq "pgx\|pq\|database/sql.*postgres\|psycopg\|pg.*Pool\|sequelize.*postgres\|TypeORM.*postgres" . 2>/dev/null && echo "postgres detected"
|
|
|
|
# MySQL
|
|
grep -rq "mysql\|mariadb" --include="*.go" --include="*.py" --include="*.ts" . 2>/dev/null && echo "mysql detected"
|
|
|
|
# Redis
|
|
grep -rq "go-redis\|redigo\|redis\|ioredis\|bull" --include="*.go" --include="*.py" --include="*.ts" . 2>/dev/null && echo "redis detected"
|
|
|
|
# MongoDB
|
|
grep -rq "mongo\|bson\|mongoose" --include="*.go" --include="*.py" --include="*.ts" . 2>/dev/null && echo "mongodb detected"
|
|
|
|
# Kafka/RabbitMQ
|
|
grep -rq "kafka\|sarama\|confluent" . 2>/dev/null && echo "kafka detected"
|
|
grep -rq "rabbitmq\|amqp" . 2>/dev/null && echo "rabbitmq detected"
|
|
```
|
|
|
|
### Step 4: Detect Environment Variables
|
|
|
|
```bash
|
|
# Scan .env.example or .env.sample
|
|
if test -f .env.example; then
|
|
cat .env.example
|
|
elif test -f .env.sample; then
|
|
cat .env.sample
|
|
fi
|
|
|
|
# Scan code for env var references
|
|
# Go
|
|
grep -rn "os.Getenv\|os.LookupEnv\|viper.Get" --include="*.go" . 2>/dev/null | head -30
|
|
|
|
# Node.js
|
|
grep -rn "process.env\." --include="*.ts" --include="*.js" . 2>/dev/null | head -30
|
|
|
|
# Python
|
|
grep -rn "os.environ\|os.getenv\|settings\." --include="*.py" . 2>/dev/null | head -30
|
|
|
|
# Detect sensitive variables
|
|
grep -rEi "(PASSWORD|SECRET|KEY|TOKEN|CREDENTIAL|AUTH)" --include="*.go" --include="*.ts" --include="*.py" . 2>/dev/null | grep -i "getenv\|environ\|process\.env\|viper" | head -20
|
|
```
|
|
|
|
---
|
|
|
|
## Interactive Collection Flow (Mixed Mode)
|
|
|
|
### Decision Matrix: When to Ask vs When to Auto-Fill vs When to Write TODO
|
|
|
|
| Information | Detectable? | Critical? | Action |
|
|
|-------------|-------------|-----------|--------|
|
|
| Startup command | Often yes | **Yes** | Auto-detect → if fail, ask with the platform user-confirmation tool immediately |
|
|
| Health endpoint | Sometimes | **Yes** | Auto-detect → if fail, ask with the platform user-confirmation tool |
|
|
| Port | Usually | No | Auto-detect → default 8080 |
|
|
| Database type | Often | **Yes** (if code uses DB) | Auto-detect → if fail, ask with the platform user-confirmation tool |
|
|
| DB connection URL | No | **Yes** | Mark `requires_user_input`, use `${DATABASE_URL}` |
|
|
| Redis/cache | Sometimes | No | Auto-detect → write TODO if unclear |
|
|
| API keys | No | Depends | Mark `requires_user_input`, use `${VAR_NAME}` |
|
|
| Log level | Yes (default) | No | Auto-fill with "info" |
|
|
| Feature flags | Sometimes | No | Write TODO placeholder |
|
|
|
|
### User Confirmation Templates
|
|
|
|
In Codex, use `request_user_input`. On other platforms, use the equivalent user-confirmation
|
|
tool. If no confirmation tool is available, record assumptions and required follow-up in
|
|
`environment.json`.
|
|
|
|
**Template 1: Startup Command (critical, must ask if not detected)**
|
|
|
|
```json
|
|
{
|
|
"question": "Unable to automatically detect how to start this project. How is the application started for development?",
|
|
"header": "Startup",
|
|
"options": [
|
|
{
|
|
"label": "Custom command",
|
|
"description": "I'll provide the specific command (e.g., 'go run ./cmd/server', 'npm run dev')"
|
|
},
|
|
{
|
|
"label": "Docker Compose",
|
|
"description": "The project uses docker-compose up to start everything"
|
|
},
|
|
{
|
|
"label": "Makefile target",
|
|
"description": "There's a Makefile with run/start/dev targets"
|
|
},
|
|
{
|
|
"label": "Not applicable",
|
|
"description": "This is a library/package — no startup command needed"
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
**Template 2: Database Dependency (critical if DB usage detected in code)**
|
|
|
|
```json
|
|
{
|
|
"question": "Detected database usage in code ({detected_db_type}). Please confirm the database setup:",
|
|
"header": "Database",
|
|
"options": [
|
|
{
|
|
"label": "Docker container",
|
|
"description": "Use Docker to run {db_type} locally (recommended for development)"
|
|
},
|
|
{
|
|
"label": "Local installation",
|
|
"description": "Database is installed directly on this machine"
|
|
},
|
|
{
|
|
"label": "Remote/cloud",
|
|
"description": "Database is hosted remotely (staging/dev environment)"
|
|
},
|
|
{
|
|
"label": "SQLite/embedded",
|
|
"description": "Use an embedded database for development/testing"
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
**Template 3: Sensitive Configuration (always ask if detected)**
|
|
|
|
```json
|
|
{
|
|
"question": "Detected sensitive environment variables in code: {var_list}. These are needed for the application to run. How should they be configured?",
|
|
"header": "Secrets",
|
|
"options": [
|
|
{
|
|
"label": "Environment variables (recommended)",
|
|
"description": "Reference via ${VAR_NAME} — you set them in your shell profile"
|
|
},
|
|
{
|
|
"label": "Safe test values available",
|
|
"description": "Some of these have safe test/development values that can be used"
|
|
},
|
|
{
|
|
"label": "Config file reference",
|
|
"description": "Reference a local config file like ~/.config/app/secrets.json"
|
|
},
|
|
{
|
|
"label": "Skip for now",
|
|
"description": "Mark as TODO — fill in later before running verification"
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Startup Scripts Generation
|
|
|
|
### setup-env.sh
|
|
|
|
```bash
|
|
#!/usr/bin/env bash
|
|
# Environment setup script — starts required services
|
|
# Generated by ecl-harness-engineer, consumed by harness-executor
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
|
|
|
echo "=== Setting up environment for {project_name} ==="
|
|
|
|
# Start services via Docker (if docker-compose exists)
|
|
if [ -f "$PROJECT_ROOT/docker-compose.yml" ]; then
|
|
echo "Starting services via docker-compose..."
|
|
docker-compose -f "$PROJECT_ROOT/docker-compose.yml" up -d
|
|
fi
|
|
|
|
# Or start individual services
|
|
# {auto-generated based on detected services}
|
|
|
|
# Wait for services to be ready
|
|
echo "Waiting for services..."
|
|
# {auto-generated health checks}
|
|
|
|
# Run migrations (if applicable)
|
|
# {auto-generated migration commands}
|
|
|
|
echo "=== Environment ready ==="
|
|
```
|
|
|
|
### start-server.sh
|
|
|
|
```bash
|
|
#!/usr/bin/env bash
|
|
# Application startup script
|
|
# Generated by ecl-harness-engineer, consumed by harness-executor
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
|
|
|
cd "$PROJECT_ROOT"
|
|
|
|
# Set default environment variables
|
|
export PORT="${PORT:-8080}"
|
|
export ENV="${ENV:-development}"
|
|
export LOG_LEVEL="${LOG_LEVEL:-debug}"
|
|
|
|
# Start the application
|
|
echo "Starting {project_name} on port $PORT..."
|
|
{startup_command}
|
|
```
|
|
|
|
### teardown-env.sh
|
|
|
|
```bash
|
|
#!/usr/bin/env bash
|
|
# Environment teardown script
|
|
# Generated by ecl-harness-engineer, consumed by harness-executor
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
|
|
|
echo "=== Tearing down environment ==="
|
|
|
|
# Stop Docker services
|
|
if [ -f "$PROJECT_ROOT/docker-compose.yml" ]; then
|
|
docker-compose -f "$PROJECT_ROOT/docker-compose.yml" down -v
|
|
fi
|
|
|
|
# Clean up optional runtime verification artifacts when advanced tracing is enabled
|
|
if [ -d "$PROJECT_ROOT/harness/trace" ]; then
|
|
rm -rf "$PROJECT_ROOT/harness/trace/verify-report.json" 2>/dev/null
|
|
rm -rf "$PROJECT_ROOT/harness/trace/verification-report.json" 2>/dev/null
|
|
fi
|
|
|
|
echo "=== Environment cleaned up ==="
|
|
```
|
|
|
|
---
|
|
|
|
## Readiness Check Types
|
|
|
|
| Type | When to Use | Config |
|
|
|------|-------------|--------|
|
|
| `http` | Web API with health endpoint | `{ "endpoint": "/health", "port": 8080, "expected_status": 200 }` |
|
|
| `tcp` | Service that listens on a port but no HTTP | `{ "host": "localhost", "port": 5432 }` |
|
|
| `log_pattern` | Service that logs a "ready" message | `{ "pattern": "Server listening on", "timeout_seconds": 30 }` |
|
|
| `process` | Just check the process is running | `{ "command": "pgrep -f 'my-app'" }` |
|
|
| `none` | Library or no startup needed | (omit readiness section) |
|
|
|
|
---
|
|
|
|
## Sensitive Configuration Security
|
|
|
|
> **Core rule: never hardcode sensitive values in environment.json or scripts.**
|
|
|
|
### Safe Patterns
|
|
|
|
| Pattern | Syntax | Example |
|
|
|---------|--------|---------|
|
|
| Environment variable | `${VAR_NAME}` | `"password": "${DB_PASSWORD}"` |
|
|
| With default | `${VAR_NAME:-default}` | `"port": "${PORT:-8080}"` |
|
|
| Config file ref | `$file:path:key` | `"key": "$file:~/.config/app/secrets.json:api.key"` |
|
|
|
|
### Detection and Marking
|
|
|
|
When sensitive variables are detected in code, mark them in `_meta.requires_user_input`:
|
|
|
|
```json
|
|
{
|
|
"_meta": {
|
|
"requires_user_input": ["DATABASE_URL", "JWT_SECRET", "API_KEY"],
|
|
"todos": [
|
|
"Set DATABASE_URL environment variable before running verification",
|
|
"Configure JWT_SECRET for authentication testing"
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
### What NOT to Put in environment.json
|
|
|
|
- Actual passwords, API keys, tokens
|
|
- Connection strings with embedded credentials
|
|
- Test credentials that might be valid
|
|
- Internal URLs that should not be in version control
|
|
|
|
---
|
|
|
|
## Autonomous Mode (User Confirmation Tool Not Available)
|
|
|
|
When no user-confirmation tool is available:
|
|
|
|
1. **Auto-detect everything possible** — use all detection strategies above
|
|
2. **Apply conservative defaults** — use most common values
|
|
3. **Mark unknowns as TODO** — never guess at critical config
|
|
4. **Document assumptions** — explain what was detected and what's assumed
|
|
|
|
```json
|
|
{
|
|
"_meta": {
|
|
"generated_by": "ecl-harness-engineer",
|
|
"mode": "autonomous",
|
|
"assumptions": [
|
|
"Startup command inferred from cmd/server/main.go",
|
|
"Port 8080 assumed (most common for Go web servers)",
|
|
"PostgreSQL detected from pgx import in internal/storage/"
|
|
],
|
|
"requires_user_input": ["DATABASE_URL"],
|
|
"todos": [
|
|
"Verify startup command is correct",
|
|
"Confirm PostgreSQL connection details",
|
|
"Set DATABASE_URL environment variable"
|
|
]
|
|
}
|
|
}
|
|
```
|