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