Skip to content
Open
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
3 changes: 0 additions & 3 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -151,9 +151,6 @@ RATE_LIMIT_STANDARD=100/minute
# Rate limit for AI/expensive operations like chat (default: 20/minute)
RATE_LIMIT_AI=20/minute

# Rate limit for WebSocket connections (default: 30/minute)
RATE_LIMIT_WEBSOCKET=30/minute

# Storage backend for rate limiting (default: memory)
# Options: memory (single instance) or redis (distributed)
RATE_LIMIT_STORAGE=memory
Expand Down
15 changes: 4 additions & 11 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -368,15 +368,6 @@ CODEFRAME_BOOTSTRAP_TOKEN=<secret> # Out-of-band secret gating the
# account"; `cf auth register
# --bootstrap-token` sends the header.

# Test-only endpoints (#753) — default OFF
CODEFRAME_ENABLE_TEST_ENDPOINTS=1 # Registers the integration-test-only
# POST /test/broadcast route (pushes a WS
# broadcast to all subscribers). Read once
# at import time; unset = route absent
# (404, not in OpenAPI). Never set in
# production — leave unset except in CI /
# WebSocket integration test runs.

# Experimental cloud engine (#966) — default OFF (refuse)
CODEFRAME_ENABLE_CLOUD_ENGINE=1 # Unlock `--engine cloud` (the E2B adapter).
# Off by default and off the advertised
Expand Down Expand Up @@ -518,8 +509,10 @@ JWT_LIFETIME_SECONDS=86400 # JWT validity window; default 24h (was 7d
# tells the client to drop its copy;
# verification is stateless signature
# checking with no server-side denylist
# (the `sessions` table is unused), so a
# leaked token stays valid for up to this
# (there is no session store at all — the
# vestigial `sessions` table was dropped
# in #968), so a leaked token stays valid
# for up to this
# lifetime. This value IS the mitigation —
# lower it if tokens may leak.

Expand Down
3 changes: 2 additions & 1 deletion codeframe/auth/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -362,7 +362,8 @@ def get_jwt_strategy() -> JWTStrategy:
**Known limitation — logout does not revoke.** ``POST /auth/jwt/logout``
only tells the client to drop its copy; the token itself stays valid until
it expires, because verification is stateless signature-checking with no
server-side denylist (the ``sessions`` table is unused). A leaked token is
server-side denylist (there is no session store at all — the vestigial
``sessions`` table was dropped in #968). A leaked token is
therefore usable for up to JWT_LIFETIME_SECONDS. The mitigating control is
that lifetime — default 24h (#657), and now actually configurable — plus
the web UI's CSP against XSS exfiltration. Real revocation needs a
Expand Down
4 changes: 0 additions & 4 deletions codeframe/config/rate_limits.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
RATE_LIMIT_AUTH: Rate limit for authentication endpoints (default: 10/minute)
RATE_LIMIT_STANDARD: Rate limit for standard API endpoints (default: 100/minute)
RATE_LIMIT_AI: Rate limit for AI/expensive operations (default: 20/minute)
RATE_LIMIT_WEBSOCKET: Rate limit for WebSocket connections (default: 30/minute)
RATE_LIMIT_STORAGE: Storage backend - memory or redis (default: memory)
RATE_LIMIT_TRUSTED_PROXIES: Comma-separated trusted proxy IPs/CIDRs
REDIS_URL: Redis connection URL for distributed rate limiting (optional)
Expand All @@ -32,7 +31,6 @@ class RateLimitConfig:
auth_limit: Rate limit for authentication endpoints
standard_limit: Rate limit for standard API endpoints
ai_limit: Rate limit for AI/expensive operations
websocket_limit: Rate limit for WebSocket connections
enabled: Whether rate limiting is enabled
storage: Storage backend ('memory' or 'redis')
redis_url: Redis connection URL for distributed rate limiting
Expand All @@ -42,7 +40,6 @@ class RateLimitConfig:
auth_limit: str = "10/minute"
standard_limit: str = "100/minute"
ai_limit: str = "20/minute"
websocket_limit: str = "30/minute"
enabled: bool = True
storage: str = "memory"
redis_url: Optional[str] = None
Expand Down Expand Up @@ -128,7 +125,6 @@ def from_global_config(cls) -> "RateLimitConfig":
auth_limit=global_config.rate_limit_auth,
standard_limit=global_config.rate_limit_standard,
ai_limit=global_config.rate_limit_ai,
websocket_limit=global_config.rate_limit_websocket,
enabled=enabled,
storage=storage,
redis_url=redis_url,
Expand Down
2 changes: 0 additions & 2 deletions codeframe/core/__init__.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
"""Core components for CodeFRAME orchestration."""

from codeframe.core.config import Config
from codeframe.core.models import Task, TaskStatus, AgentMaturity
from codeframe.core.stall_detector import StallAction, StallDetectedError, StallDetector

__all__ = [
"Config",
"Task",
"TaskStatus",
"AgentMaturity",
Expand Down
229 changes: 11 additions & 218 deletions codeframe/core/config.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
"""Configuration management for CodeFRAME.

This module provides two configuration systems:
1. Legacy v1 config (JSON-based): ProjectConfig, GlobalConfig
1. Process config (environment-driven): GlobalConfig, read from the environment
and ``.env`` — secrets, rate limits, CORS, log level.
2. v2 environment config (YAML-based): EnvironmentConfig

v2 environment config is stored in .codeframe/config.yaml and controls:
Expand All @@ -11,7 +12,6 @@
- Lint tools (ruff, eslint, prettier)
"""

import json
import logging
from dataclasses import (
dataclass,
Expand All @@ -24,10 +24,10 @@
from typing import Any, Optional

import yaml
from pydantic import BaseModel, Field, field_validator
from pydantic import Field, field_validator
from pydantic_settings import BaseSettings, SettingsConfigDict

from codeframe.core.atomic_io import atomic_write_json, atomic_write_text
from codeframe.core.atomic_io import atomic_write_text

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -605,70 +605,14 @@ def get_default_environment_config() -> EnvironmentConfig:


# =============================================================================
# v1 Legacy Configuration (JSON-based)
# Process configuration (environment-driven)
# =============================================================================


class ProviderConfig(BaseModel):
"""LLM provider configuration."""

lead_agent: str = "claude"
backend_agent: str = "claude"
frontend_agent: str = "gpt4"
test_agent: str = "claude"
review_agent: str = "gpt4"


class AgentPolicyConfig(BaseModel):
"""Global agent management policies."""

require_review_below_maturity: str = "supporting"
allow_full_autonomy: bool = False


class InterruptionConfig(BaseModel):
"""Interruption mode configuration."""

enabled: bool = True
sync_blockers: list[str] = Field(default_factory=lambda: ["requirement", "security"])
async_blockers: list[str] = Field(default_factory=lambda: ["technical", "external"])
auto_continue: bool = True


class NotificationChannelConfig(BaseModel):
"""Notification channel configuration."""

enabled: bool = True
channels: list[str] = Field(default_factory=list)
webhook_url: Optional[str] = None
batch_interval: Optional[int] = None


class NotificationsConfig(BaseModel):
"""Multi-channel notification configuration."""

sync_blockers: NotificationChannelConfig = Field(default_factory=NotificationChannelConfig)
async_blockers: NotificationChannelConfig = Field(default_factory=NotificationChannelConfig)


class CheckpointConfig(BaseModel):
"""Checkpoint configuration."""

auto_save_interval: int = 1800 # seconds
pre_compactification: bool = True
per_task_completion: bool = True


class ProjectConfig(BaseModel):
"""Project-specific configuration."""

project_name: str
project_type: str = "python"
providers: ProviderConfig = Field(default_factory=ProviderConfig)
agent_policy: AgentPolicyConfig = Field(default_factory=AgentPolicyConfig)
interruption_mode: InterruptionConfig = Field(default_factory=InterruptionConfig)
notifications: NotificationsConfig = Field(default_factory=NotificationsConfig)
checkpoints: CheckpointConfig = Field(default_factory=CheckpointConfig)
#
# The JSON-file config layer that used to live here — ProjectConfig and its
# ProviderConfig / AgentPolicyConfig / InterruptionConfig / NotificationsConfig /
# CheckpointConfig members, read and written by a `Config` manager against
# .codeframe/config.json — was deleted in #968. Nothing in the product read it;
# workspace settings live in .codeframe/config.yaml via EnvironmentConfig above.


class GlobalConfig(BaseSettings):
Expand All @@ -693,7 +637,6 @@ class GlobalConfig(BaseSettings):
# file access and agent execution. Set API_HOST=0.0.0.0 to expose it.
api_host: str = Field("127.0.0.1", alias="API_HOST")
api_port: int = Field(8080, alias="API_PORT")
cors_origins: str = Field("http://localhost:3000,http://localhost:5173", alias="CORS_ORIGINS")

# Logging configuration
log_level: str = Field("INFO", alias="LOG_LEVEL")
Expand All @@ -718,7 +661,6 @@ class GlobalConfig(BaseSettings):
rate_limit_auth: str = Field("10/minute", alias="RATE_LIMIT_AUTH")
rate_limit_standard: str = Field("100/minute", alias="RATE_LIMIT_STANDARD")
rate_limit_ai: str = Field("20/minute", alias="RATE_LIMIT_AI")
rate_limit_websocket: str = Field("30/minute", alias="RATE_LIMIT_WEBSOCKET")
# Comma-separated list of trusted proxy IPs/CIDRs (e.g., "10.0.0.0/8,172.16.0.0/12")
rate_limit_trusted_proxies: str = Field("", alias="RATE_LIMIT_TRUSTED_PROXIES")

Expand Down Expand Up @@ -753,59 +695,6 @@ def validate_rate_limit_storage(cls, v: str) -> str:
raise ValueError(f"RATE_LIMIT_STORAGE must be one of {allowed}, got: {v}")
return v

def get_cors_origins_list(self) -> list[str]:
"""Parse CORS origins from comma-separated string."""
return [origin.strip() for origin in self.cors_origins.split(",") if origin.strip()]

def validate_required_for_sprint(self, sprint: int = 1) -> None:
"""Validate that required configuration is present for a given sprint.

Args:
sprint: Sprint number (1-8)

Raises:
ValueError: If required configuration is missing
"""
errors = []

if sprint >= 1:
# Sprint 1 requires Anthropic API key for Lead Agent
if not self.anthropic_api_key:
errors.append(
"ANTHROPIC_API_KEY is required for Sprint 1 (Lead Agent with Claude).\n"
" Get your API key at: https://console.anthropic.com/\n"
" Then add it to your .env file (see .env.example)"
)

if sprint >= 4:
# Sprint 4+ may require OpenAI for multi-agent
if not self.openai_api_key and not self.anthropic_api_key:
errors.append(
"At least one AI provider API key is required.\n"
" ANTHROPIC_API_KEY or OPENAI_API_KEY must be set."
)

if sprint >= 5:
# Sprint 5+ may require notification services
pass # Notifications are optional, will use webhook fallback

if errors:
error_msg = "\n\n".join(errors)
raise ValueError(
f"\n{'='*70}\nCONFIGURATION ERROR\n{'='*70}\n\n{error_msg}\n\n{'='*70}\n"
)

def ensure_directories(self) -> None:
"""Ensure required directories exist."""
# Create database directory
db_path = Path(self.database_path)
db_path.parent.mkdir(parents=True, exist_ok=True)

# Create log directory if log file is specified
if self.log_file:
log_path = Path(self.log_file)
log_path.parent.mkdir(parents=True, exist_ok=True)


def load_environment(env_file: str = ".env") -> None:
"""Load environment variables from .env file.
Expand All @@ -830,102 +719,6 @@ def load_environment(env_file: str = ".env") -> None:
load_env_files(explicit_file=env_path)


class Config:
"""Configuration manager for CodeFRAME."""

def __init__(self, project_dir: Path):
self.project_dir = project_dir
self.config_dir = project_dir / ".codeframe"
self.config_file = self.config_dir / "config.json"
self._project_config: Optional[ProjectConfig] = None
self._global_config: Optional[GlobalConfig] = None

# Load environment variables
load_environment()

def load(self) -> ProjectConfig:
"""Load project configuration."""
if self._project_config:
return self._project_config

if not self.config_file.exists():
raise FileNotFoundError(f"Config not found: {self.config_file}")

with open(self.config_file) as f:
data = json.load(f)
self._project_config = ProjectConfig(**data)

return self._project_config

def save(self, config: ProjectConfig) -> None:
"""Save project configuration."""
self.config_dir.mkdir(parents=True, exist_ok=True)
# Atomic (#954) — see save_environment_config.
atomic_write_json(self.config_file, config.model_dump())
self._project_config = config

def get_global(self) -> GlobalConfig:
"""Load global configuration from environment variables.

Returns:
GlobalConfig instance with values from environment

Raises:
ValueError: If required configuration is missing
"""
if not self._global_config:
self._global_config = GlobalConfig()
return self._global_config

def validate_for_sprint(self, sprint: int = 1) -> None:
"""Validate configuration for a specific sprint.

Args:
sprint: Sprint number to validate for

Raises:
ValueError: If required configuration is missing
"""
global_config = self.get_global()
global_config.validate_required_for_sprint(sprint)
global_config.ensure_directories()

def set(self, key: str, value: Any) -> None:
"""Set configuration value using dot notation."""
config = self.load()
keys = key.split(".")
obj = config.model_dump()

# Navigate to the correct nested dict
current = obj
for k in keys[:-1]:
if k not in current:
current[k] = {}
current = current[k]

# Set the value
current[keys[-1]] = value

# Reload and save
self._project_config = ProjectConfig(**obj)
self.save(self._project_config)

def get(self, key: str) -> Any:
"""Get configuration value using dot notation."""
config = self.load()
keys = key.split(".")
obj = config.model_dump()

current = obj
for k in keys:
if k not in current:
return None
current = current[k]

return current


# Module-level singleton for GlobalConfig
_global_config: Optional[GlobalConfig] = None


Expand Down
1 change: 0 additions & 1 deletion codeframe/core/env_provenance.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,6 @@ def vet_env_base_url(env_var: str) -> Optional[str]:
# Whether the guards run at all
"CODEFRAME_AUTH_REQUIRED", # `false` disables authentication
"CODEFRAME_DEPLOYMENT_MODE", # flips hosted-mode gating
"CODEFRAME_ENABLE_TEST_ENDPOINTS", # arms /test/broadcast (#753)
"WORKSPACE_ROOT", # the workspace allowlist (#655/#896)
# Where code is found and run
"PATH",
Expand Down
Loading
Loading