config.py
python
sha256:7d5985ef251de9f0154f9b185a75cf36174bf73e0c34f5eca133e7bd20224bf4
Merge branch 'feat/opengraph-repo-cards' into dev
Human
23 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 | # Open Graph card renderer: "auto" (stub in test, Playwright otherwise), "playwright", or "stub". |
| 92 | opengraph_renderer: str = "auto" |
| 93 | |
| 94 | # Database connection pool — pool_timeout is how long to wait for a |
| 95 | # connection from the pool before raising TimeoutError. |
| 96 | db_pool_timeout: int = 30 # seconds |
| 97 | |
| 98 | # Slow query log threshold. Any SQL statement taking longer than this |
| 99 | # many milliseconds is logged at WARNING level with the full statement |
| 100 | # and elapsed time. Set to 0 to disable. |
| 101 | slow_query_threshold_ms: int = 100 |
| 102 | |
| 103 | # Per-user storage quota enforced at the MCP muse_push layer. |
| 104 | # Agents that loop indefinitely cannot fill the disk beyond this limit. |
| 105 | # Set to 0 to disable quota enforcement (not recommended in production). |
| 106 | mcp_push_per_user_quota_bytes: int = 10 * 1024 * 1024 * 1024 # 10 GB default |
| 107 | |
| 108 | # Per-repo storage quota enforced at both MCP and wire push layers. |
| 109 | # Prevents a single repo from monopolising disk regardless of user quota. |
| 110 | # Set to 0 to disable. |
| 111 | per_repo_quota_bytes: int = 5 * 1024 * 1024 * 1024 # 5 GB default |
| 112 | |
| 113 | # MPack push size gates — enforced at the route layer before any storage I/O. |
| 114 | mpack_max_bytes: int = 512 * 1024 * 1024 # 512 MB per mpack |
| 115 | mpack_max_commits: int = 100_000 # commits per mpack push |
| 116 | mpack_max_objects: int = 1_000_000 # objects per mpack push |
| 117 | |
| 118 | # Sync-path content_cache threshold. Mpacks at or below this size have their |
| 119 | # objects written inline (content_cache=raw_bytes, storage_uri='pending') so |
| 120 | # fetch requests are served immediately without waiting for the background job. |
| 121 | # Mpacks above this threshold skip inline writes; the background job handles them. |
| 122 | mpack_content_cache_max_bytes: int = 4 * 1024 * 1024 # 4 MB |
| 123 | |
| 124 | # Maximum total decompressed size for all objects in an mpack. |
| 125 | # Mpacks that exceed this limit during decompression are quarantined as |
| 126 | # potential zip bombs (Phase 2 content validation). |
| 127 | mpack_max_decompressed_bytes: int = 4 * 1024 * 1024 * 1024 # 4 GB |
| 128 | |
| 129 | # Per-user daily mpack upload byte limit (Phase 4a). |
| 130 | # mpack-presign returns 429 when the caller's running daily total would |
| 131 | # exceed this value. Set to 0 to disable enforcement. |
| 132 | mpack_daily_upload_limit_bytes: int = 50 * 1024 * 1024 * 1024 # 50 GB default |
| 133 | |
| 134 | # Soft-delete retention window: objects are hard-deleted this many days |
| 135 | # after their deleted_at timestamp is set. |
| 136 | object_retention_days: int = 30 |
| 137 | |
| 138 | # Stdio MCP server: proxy DAW tools to Muse backend |
| 139 | muse_mcp_url: str | None = None |
| 140 | mcp_token: str | None = None |
| 141 | |
| 142 | musehub_releases_dir: str = "/data/releases" |
| 143 | |
| 144 | # Root directory for per-repo on-disk state (branch refs, MERGE_STATE, worktrees). |
| 145 | # Objects are in the blob store (R2/MinIO) — not here. |
| 146 | musehub_repos_dir: str = "/data/repos" |
| 147 | |
| 148 | # Webhook secret encryption key — AES-256 (Fernet) key for encrypting webhook signing |
| 149 | # secrets at rest. Generate with: |
| 150 | # python3 -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())" |
| 151 | webhook_secret_key: str | None = None |
| 152 | |
| 153 | # Commit signature enforcement. |
| 154 | # When True, wire_push rejects any commit that does not carry a non-empty |
| 155 | # ``signature`` and ``signer_key_id``. Recommended for production repos |
| 156 | # where all contributors have registered signing keys. |
| 157 | # Default False for backward compatibility with existing unsigned commits. |
| 158 | require_signed_commits: bool = False |
| 159 | |
| 160 | # Agent identity registry for impersonation detection. |
| 161 | # A JSON list of known/trusted agent_id prefixes or exact IDs |
| 162 | # (e.g. '["agentception-worker", "claude-opus-4-6"]'). |
| 163 | # When non-empty, commits whose agent_id does NOT match any entry are |
| 164 | # accepted but flagged in the commit metadata with "untrusted_agent": true. |
| 165 | # Unknown agents are NEVER rejected — only flagged. |
| 166 | # Leave empty (default) to accept all agent_ids without flagging. |
| 167 | trusted_agent_ids: list[str] = [] |
| 168 | |
| 169 | @model_validator(mode="after") |
| 170 | def _warn_missing_production_secrets(self) -> "Settings": |
| 171 | """Warn at startup when optional-but-recommended secrets are absent in production.""" |
| 172 | is_prod = not self.debug and self.muse_env not in ("test", "development") |
| 173 | if not is_prod: |
| 174 | return self |
| 175 | _log = logging.getLogger(__name__) |
| 176 | if not self.webhook_secret_key: |
| 177 | _log.warning( |
| 178 | "WEBHOOK_SECRET_KEY is not set — webhook delivery will be disabled. " |
| 179 | "Generate with: python3 -c \"from cryptography.fernet import Fernet; " |
| 180 | "print(Fernet.generate_key().decode())\"" |
| 181 | ) |
| 182 | return self |
| 183 | |
| 184 | model_config = SettingsConfigDict( |
| 185 | env_file=".env", |
| 186 | env_file_encoding="utf-8", |
| 187 | extra="ignore", # silently discard unknown env vars (e.g. OPENROUTER_API_KEY from other tools) |
| 188 | ) |
| 189 | |
| 190 | |
| 191 | @lru_cache() |
| 192 | def get_settings() -> Settings: |
| 193 | """Get cached settings instance.""" |
| 194 | return Settings() |
| 195 | |
| 196 | |
| 197 | # Convenience access |
| 198 | settings = get_settings() |
File History
15 commits
sha256:7d5985ef251de9f0154f9b185a75cf36174bf73e0c34f5eca133e7bd20224bf4
Merge branch 'feat/opengraph-repo-cards' into dev
Human
23 days ago
sha256:7c5915d3a65660061405c2cc04bcb297f7c97157c71629b856bd0261c4cc53ac
docs: add v0.2.0-nightly.3 changelog
Sonnet 5
55 days ago
sha256:fc04e4cae9e1774d6a21b65c45daeed0e6787eb581d13aa1b03bfe9384a34226
Merge branch 'fix/two-column-scroll-layout' into dev
Human
55 days ago
sha256:408916fc5973ba59c6e4eebaa80ebdcc801c0a63205651e25009d11548f79454
chore: bump version to 0.2.0.dev2 — nightly.2, matching muse
Sonnet 4.6
patch
58 days ago
sha256:d035733f21ccff27735fddebfbbe0ed24565a32a22db8de5885402262671ecd2
chore: bump version to 0.2.0rc15 for musehub#113 fix release
Sonnet 4.6
patch
61 days ago
sha256:0032d6cfa33bc3c8367436ad768e7dd0e339b4332153160247da8266cb5fa352
Merge branch 'task/version-tags-phase3-server' into dev
Human
63 days ago
sha256:4669620efda9ff41c55bdefd1f7bfe1c239d468428744c84ead9957e5a003a53
merge: rescue snapshot-recovery hardening (c00aa21d) into d…
Opus 4.8
minor
⚠
76 days ago
sha256:a59da49c4611b970fc4b6ae48678ce4943261c213a07ddbd73ce9201df869b4a
fix: remove false-positive proposal_comments index drop fro…
Sonnet 4.6
patch
80 days ago
sha256:0a240d6dbff234f07d98a28a4a9a68db702f3f9ff9260196f24219bdb1c0b6f3
feat: render markdown mists as HTML with heading anchor links
Sonnet 4.6
patch
81 days ago
sha256:24a7d47486ebc4ebd1832830580e177ec6f877b48dced8c000e198cdec4ce9d6
Merge 'task/bump-version-rc12' into 'dev' — proposal: Bump …
Human
82 days ago
sha256:b9ff931d147e0114a1f17060f415b89ed551c170a91ff226c70437aa5c85f9ee
Merge 'task/bump-version-rc12' into 'dev' — proposal: Bump …
Human
82 days ago
sha256:d1122d21e73471879b460037b22c0b50fded7c423444a176f248428f75dac39c
Merge 'task/fix-issue-pagination-cursor' into 'dev' — propo…
Human
82 days ago
sha256:01e18975e73d2b3cd5b6db7929c895bef9aa6e0d4391dc5b2adfc548b41318dd
Merge 'feat/adding-debug-logs-to-staging' into 'dev' — prop…
Human
82 days ago
sha256:6b1949fc2797ca4c1936a637a4cbfec828ef56cf52398a2e74ca3c4f494e728f
fix: use wire_bytes not mpack_bytes_raw in compute_object_b…
Sonnet 4.6
patch
94 days ago
sha256:b99f2455dc346966d040133f5203297e6e3ef5803a93728a2c30568d0a0f7583
rename: delta_add → delta_upsert across wire format, models…
Sonnet 4.6
patch
97 days ago