config.py
python
sha256:25d96102cb2d69a038356dff26f4633156da2f1faf98fe0d0e4438ff3f367f12
refactor: rename 0054/0055 migrations to standard convention
Sonnet 4.6
minor
⚠ breaking
21 days ago
| 1 | """ |
| 2 | Muse Configuration |
| 3 | |
| 4 | Environment-based configuration for the Muse service. |
| 5 | """ |
| 6 | |
| 7 | import logging |
| 8 | from functools import lru_cache |
| 9 | |
| 10 | from pydantic import model_validator |
| 11 | from pydantic_settings import BaseSettings, SettingsConfigDict |
| 12 | |
| 13 | |
| 14 | def _app_version_from_package() -> str: |
| 15 | """Read version from the single source of truth (pyproject.toml via protocol.version).""" |
| 16 | from musehub.protocol.version import MUSE_VERSION |
| 17 | return MUSE_VERSION |
| 18 | |
| 19 | |
| 20 | class Settings(BaseSettings): |
| 21 | """Application settings loaded from environment variables.""" |
| 22 | |
| 23 | # Service Info |
| 24 | app_name: str = "Muse" |
| 25 | app_version: str = _app_version_from_package() |
| 26 | debug: bool = False |
| 27 | muse_env: str = "production" # "test" | "development" | "production" |
| 28 | |
| 29 | # Public base URL — used to build clone URLs returned by the API and MCP tools. |
| 30 | # Override via PUBLIC_URL env var on staging/local: e.g. https://staging.musehub.ai |
| 31 | public_url: str = "https://musehub.ai" |
| 32 | |
| 33 | # Allowlist of Host header values that the server will trust when building |
| 34 | # request-derived URLs (e.g. clone URLs on the repo home page). |
| 35 | # Any Host value not in this set falls back to public_url. |
| 36 | # Override via ALLOWED_HOSTS env var (JSON array): |
| 37 | # ALLOWED_HOSTS='["musehub.ai","staging.musehub.ai","localhost:1337"]' |
| 38 | allowed_hosts: list[str] = ["musehub.ai", "staging.musehub.ai", "localhost:1337"] |
| 39 | |
| 40 | # Server Configuration |
| 41 | host: str = "0.0.0.0" |
| 42 | port: int = 10001 |
| 43 | |
| 44 | # Database Configuration — PostgreSQL only |
| 45 | # Example: postgresql+asyncpg://user:pass@localhost:5432/musehub |
| 46 | database_url: str | None = None |
| 47 | db_password: str | None = None |
| 48 | |
| 49 | # CORS Settings (fail closed: no default origins) |
| 50 | # Set CORS_ORIGINS (JSON array) in .env. Local dev: ["https://localhost:1337", "muse://"]. |
| 51 | # Production: exact origins only. Never use "*" in production. |
| 52 | cors_origins: list[str] = [] |
| 53 | |
| 54 | @model_validator(mode="after") |
| 55 | def _warn_cors_wildcard_in_production(self) -> "Settings": |
| 56 | """Warn when CORS allows all origins in non-debug (production) mode.""" |
| 57 | if not self.debug and self.cors_origins and "*" in self.cors_origins: |
| 58 | logging.getLogger(__name__).warning( |
| 59 | "CORS allows all origins (*) with DEBUG=false. " |
| 60 | "Set CORS_ORIGINS to exact origins in production." |
| 61 | ) |
| 62 | return self |
| 63 | |
| 64 | # AWS S3 Asset Delivery (drum kits, GM soundfont) |
| 65 | # Region MUST match the bucket's region (S3 returns 301 if URL uses wrong region). |
| 66 | aws_region: str = "eu-west-1" |
| 67 | aws_s3_asset_bucket: str | None = None |
| 68 | aws_cloudfront_domain: str | None = None |
| 69 | presign_expiry_seconds: int = 1800 # 30-min default for presigned download URLs |
| 70 | |
| 71 | # S3-compatible blob storage (Cloudflare R2, MinIO, AWS S3, etc.). |
| 72 | # When blob_storage_bucket is set it is used for all muse object blobs. |
| 73 | blob_storage_bucket: str | None = None |
| 74 | blob_storage_endpoint: str | None = None # e.g. https://<account>.r2.cloudflarestorage.com or http://minio:9000 |
| 75 | blob_storage_public_endpoint: str | None = None # public URL for presigned URLs (local dev: http://localhost:9000) |
| 76 | blob_storage_cdn_base_url: str | None = None # CDN origin for mpack GET URLs (e.g. https://cdn.musehub.ai) |
| 77 | blob_storage_access_key_id: str | None = None |
| 78 | blob_storage_secret_access_key: str | None = None |
| 79 | blob_storage_region: str = "auto" |
| 80 | |
| 81 | # Asset endpoint rate limits (device-ID auth) |
| 82 | asset_rate_limit_per_device: str = "30/minute" |
| 83 | asset_rate_limit_per_ip: str = "120/minute" |
| 84 | |
| 85 | # MCP rate limits — agents get a higher tier than anonymous/human callers. |
| 86 | # Agent identities have `identity_type == "agent"` in the DB. |
| 87 | mcp_rate_limit_human: str = "60/minute" |
| 88 | mcp_rate_limit_agent: str = "600/minute" |
| 89 | mcp_rate_limit_anonymous: str = "20/minute" |
| 90 | |
| 91 | # Database connection pool — pool_timeout is how long to wait for a |
| 92 | # connection from the pool before raising TimeoutError. |
| 93 | db_pool_timeout: int = 30 # seconds |
| 94 | |
| 95 | # Slow query log threshold. Any SQL statement taking longer than this |
| 96 | # many milliseconds is logged at WARNING level with the full statement |
| 97 | # and elapsed time. Set to 0 to disable. |
| 98 | slow_query_threshold_ms: int = 100 |
| 99 | |
| 100 | # Per-user storage quota enforced at the MCP muse_push layer. |
| 101 | # Agents that loop indefinitely cannot fill the disk beyond this limit. |
| 102 | # Set to 0 to disable quota enforcement (not recommended in production). |
| 103 | mcp_push_per_user_quota_bytes: int = 10 * 1024 * 1024 * 1024 # 10 GB default |
| 104 | |
| 105 | # Per-repo storage quota enforced at both MCP and wire push layers. |
| 106 | # Prevents a single repo from monopolising disk regardless of user quota. |
| 107 | # Set to 0 to disable. |
| 108 | per_repo_quota_bytes: int = 5 * 1024 * 1024 * 1024 # 5 GB default |
| 109 | |
| 110 | # MPack push size gates — enforced at the route layer before any storage I/O. |
| 111 | mpack_max_bytes: int = 512 * 1024 * 1024 # 512 MB per mpack |
| 112 | mpack_max_commits: int = 100_000 # commits per mpack push |
| 113 | mpack_max_objects: int = 1_000_000 # objects per mpack push |
| 114 | |
| 115 | # Sync-path content_cache threshold. Mpacks at or below this size have their |
| 116 | # objects written inline (content_cache=raw_bytes, storage_uri='pending') so |
| 117 | # fetch requests are served immediately without waiting for the background job. |
| 118 | # Mpacks above this threshold skip inline writes; the background job handles them. |
| 119 | mpack_content_cache_max_bytes: int = 4 * 1024 * 1024 # 4 MB |
| 120 | |
| 121 | # Maximum total decompressed size for all objects in an mpack. |
| 122 | # Mpacks that exceed this limit during decompression are quarantined as |
| 123 | # potential zip bombs (Phase 2 content validation). |
| 124 | mpack_max_decompressed_bytes: int = 4 * 1024 * 1024 * 1024 # 4 GB |
| 125 | |
| 126 | # Per-user daily mpack upload byte limit (Phase 4a). |
| 127 | # mpack-presign returns 429 when the caller's running daily total would |
| 128 | # exceed this value. Set to 0 to disable enforcement. |
| 129 | mpack_daily_upload_limit_bytes: int = 50 * 1024 * 1024 * 1024 # 50 GB default |
| 130 | |
| 131 | # Soft-delete retention window: objects are hard-deleted this many days |
| 132 | # after their deleted_at timestamp is set. |
| 133 | object_retention_days: int = 30 |
| 134 | |
| 135 | # Stdio MCP server: proxy DAW tools to Muse backend |
| 136 | muse_mcp_url: str | None = None |
| 137 | mcp_token: str | None = None |
| 138 | |
| 139 | musehub_releases_dir: str = "/data/releases" |
| 140 | |
| 141 | # Root directory for per-repo on-disk state (branch refs, MERGE_STATE, worktrees). |
| 142 | # Objects are in the blob store (R2/MinIO) — not here. |
| 143 | musehub_repos_dir: str = "/data/repos" |
| 144 | |
| 145 | # Webhook secret encryption key — AES-256 (Fernet) key for encrypting webhook signing |
| 146 | # secrets at rest. Generate with: |
| 147 | # python3 -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())" |
| 148 | webhook_secret_key: str | None = None |
| 149 | |
| 150 | # Commit signature enforcement. |
| 151 | # When True, wire_push rejects any commit that does not carry a non-empty |
| 152 | # ``signature`` and ``signer_key_id``. Recommended for production repos |
| 153 | # where all contributors have registered signing keys. |
| 154 | # Default False for backward compatibility with existing unsigned commits. |
| 155 | require_signed_commits: bool = False |
| 156 | |
| 157 | # Agent identity registry for impersonation detection. |
| 158 | # A JSON list of known/trusted agent_id prefixes or exact IDs |
| 159 | # (e.g. '["agentception-worker", "claude-opus-4-6"]'). |
| 160 | # When non-empty, commits whose agent_id does NOT match any entry are |
| 161 | # accepted but flagged in the commit metadata with "untrusted_agent": true. |
| 162 | # Unknown agents are NEVER rejected — only flagged. |
| 163 | # Leave empty (default) to accept all agent_ids without flagging. |
| 164 | trusted_agent_ids: list[str] = [] |
| 165 | |
| 166 | @model_validator(mode="after") |
| 167 | def _warn_missing_production_secrets(self) -> "Settings": |
| 168 | """Warn at startup when optional-but-recommended secrets are absent in production.""" |
| 169 | is_prod = not self.debug and self.muse_env not in ("test", "development") |
| 170 | if not is_prod: |
| 171 | return self |
| 172 | _log = logging.getLogger(__name__) |
| 173 | if not self.webhook_secret_key: |
| 174 | _log.warning( |
| 175 | "WEBHOOK_SECRET_KEY is not set — webhook delivery will be disabled. " |
| 176 | "Generate with: python3 -c \"from cryptography.fernet import Fernet; " |
| 177 | "print(Fernet.generate_key().decode())\"" |
| 178 | ) |
| 179 | return self |
| 180 | |
| 181 | model_config = SettingsConfigDict( |
| 182 | env_file=".env", |
| 183 | env_file_encoding="utf-8", |
| 184 | extra="ignore", # silently discard unknown env vars (e.g. OPENROUTER_API_KEY from other tools) |
| 185 | ) |
| 186 | |
| 187 | |
| 188 | @lru_cache() |
| 189 | def get_settings() -> Settings: |
| 190 | """Get cached settings instance.""" |
| 191 | return Settings() |
| 192 | |
| 193 | |
| 194 | # Convenience access |
| 195 | settings = get_settings() |
File History
2 commits
sha256:25d96102cb2d69a038356dff26f4633156da2f1faf98fe0d0e4438ff3f367f12
refactor: rename 0054/0055 migrations to standard convention
Sonnet 4.6
minor
⚠
21 days ago
sha256:4aed3d8601c8dd3ed37074de35f11f4a9699a0a4b99d43727048fd3f8e6fd13d
chore: doc sweep, ignore wrangler build state, misc fixes
Sonnet 4.6
minor
⚠
23 days ago