gabriel / musehub public
config.py python
198 lines 8.7 KB
Raw
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