config.py file-level

at main · View file ↗ · Intel ↗

History
1 files
1 commits
0 hotspots
0 🧊 dead
0 💥 blast risk
sha256:6 fix(ISR): default require_independent_second_reviewer to require Opera… · aaronrene · Sep 2, 2026
1 """Hosted dashboard config block parsing (§HGD.10.2)."""
2
3 from __future__ import annotations
4
5 from dataclasses import dataclass, field
6 from typing import Any
7
8 from tools.hosted_dashboard.hosts import validate_extra_hosts
9 from tools.hosted_dashboard.validators import (
10 DEFAULT_ORG_ENUMERATION_CAP,
11 validate_allowlist,
12 )
13
14 HOSTED_DASHBOARD_KEYS = frozenset(
15 {
16 "enabled",
17 "allow_non_loopback",
18 "cors_origins",
19 "org_allowlist",
20 "sources",
21 "enumeration_cap",
22 "musehub_hosts",
23 "musehub_base_url",
24 "max_doc_bytes",
25 }
26 )
27 SOURCE_KEYS = frozenset(
28 {
29 "github_contents",
30 "github_meta",
31 "github_checks_advisory",
32 "musehub_read",
33 }
34 )
35
36
37 class HostedDashboardConfigError(ValueError):
38 """Fail-closed hosted_dashboard config error."""
39
40
41 @dataclass(frozen=True)
42 class HostedSourcesConfig:
43 github_contents: bool = True
44 github_meta: bool = True
45 github_checks_advisory: bool = False
46 musehub_read: bool = False
47
48
49 @dataclass(frozen=True)
50 class HostedDashboardConfig:
51 """Optional ``hosted_dashboard`` section — default inert."""
52
53 enabled: bool = False
54 allow_non_loopback: bool = False
55 cors_origins: tuple[str, ...] = ()
56 org_allowlist: tuple[str, ...] = ()
57 sources: HostedSourcesConfig = field(default_factory=HostedSourcesConfig)
58 enumeration_cap: int = DEFAULT_ORG_ENUMERATION_CAP
59 musehub_hosts: frozenset[str] = field(default_factory=frozenset)
60 musehub_base_url: str | None = None
61 max_doc_bytes: int = 2_000_000
62
63 @property
64 def allowlist_pairs(self) -> list[tuple[str, str | None]]:
65 return validate_allowlist(list(self.org_allowlist))
66
67
68 def default_hosted_dashboard_config() -> HostedDashboardConfig:
69 return HostedDashboardConfig()
70
71
72 def parse_hosted_dashboard_config(raw: Any, *, path: str = "<config>") -> HostedDashboardConfig:
73 """Parse optional ``hosted_dashboard`` mapping; unknown keys fail closed."""
74 if raw is None:
75 return default_hosted_dashboard_config()
76 if not isinstance(raw, dict):
77 raise HostedDashboardConfigError(f"hosted_dashboard must be a mapping ({path})")
78
79 extra = set(raw) - HOSTED_DASHBOARD_KEYS
80 if extra:
81 raise HostedDashboardConfigError(f"unknown hosted_dashboard keys: {sorted(extra)}")
82
83 enabled = raw.get("enabled", False)
84 if not isinstance(enabled, bool):
85 raise HostedDashboardConfigError("hosted_dashboard.enabled must be a boolean")
86
87 allow_non_loopback = raw.get("allow_non_loopback", False)
88 if not isinstance(allow_non_loopback, bool):
89 raise HostedDashboardConfigError("hosted_dashboard.allow_non_loopback must be a boolean")
90
91 cors = raw.get("cors_origins", [])
92 if not isinstance(cors, list) or not all(isinstance(x, str) for x in cors):
93 raise HostedDashboardConfigError("hosted_dashboard.cors_origins must be a list of strings")
94
95 allowlist = raw.get("org_allowlist", [])
96 if not isinstance(allowlist, list) or not all(isinstance(x, str) for x in allowlist):
97 raise HostedDashboardConfigError("hosted_dashboard.org_allowlist must be a list of strings")
98 try:
99 validate_allowlist(allowlist)
100 except ValueError as exc:
101 raise HostedDashboardConfigError(str(exc)) from exc
102
103 sources = _parse_sources(raw.get("sources"))
104
105 # K7: github baseline sources must remain available when enabled.
106 if enabled and not (sources.github_contents and sources.github_meta):
107 raise HostedDashboardConfigError(
108 "hosted_dashboard.sources.github_contents and github_meta must be true (K7 baseline)"
109 )
110
111 enumeration_cap = raw.get("enumeration_cap", DEFAULT_ORG_ENUMERATION_CAP)
112 if not isinstance(enumeration_cap, int) or enumeration_cap < 1:
113 raise HostedDashboardConfigError("hosted_dashboard.enumeration_cap must be a positive integer")
114
115 muse_hosts_raw = raw.get("musehub_hosts", [])
116 if not isinstance(muse_hosts_raw, list):
117 raise HostedDashboardConfigError("hosted_dashboard.musehub_hosts must be a list of strings")
118 try:
119 muse_hosts = validate_extra_hosts([str(x) for x in muse_hosts_raw])
120 except ValueError as exc:
121 raise HostedDashboardConfigError(str(exc)) from exc
122
123 muse_base = raw.get("musehub_base_url")
124 if muse_base is not None and (not isinstance(muse_base, str) or not muse_base.strip()):
125 raise HostedDashboardConfigError("hosted_dashboard.musehub_base_url must be a non-empty string or null")
126
127 if sources.musehub_read and not muse_base:
128 raise HostedDashboardConfigError(
129 "hosted_dashboard.musehub_base_url required when sources.musehub_read is true"
130 )
131
132 max_doc = raw.get("max_doc_bytes", 2_000_000)
133 if not isinstance(max_doc, int) or max_doc < 1024:
134 raise HostedDashboardConfigError("hosted_dashboard.max_doc_bytes must be an integer >= 1024")
135
136 return HostedDashboardConfig(
137 enabled=enabled,
138 allow_non_loopback=allow_non_loopback,
139 cors_origins=tuple(cors),
140 org_allowlist=tuple(allowlist),
141 sources=sources,
142 enumeration_cap=enumeration_cap,
143 musehub_hosts=muse_hosts,
144 musehub_base_url=muse_base.strip() if isinstance(muse_base, str) else None,
145 max_doc_bytes=max_doc,
146 )
147
148
149 def _parse_sources(raw: Any) -> HostedSourcesConfig:
150 if raw is None:
151 return HostedSourcesConfig()
152 if not isinstance(raw, dict):
153 raise HostedDashboardConfigError("hosted_dashboard.sources must be a mapping")
154 extra = set(raw) - SOURCE_KEYS
155 if extra:
156 raise HostedDashboardConfigError(f"unknown hosted_dashboard.sources keys: {sorted(extra)}")
157 values: dict[str, bool] = {}
158 for key in SOURCE_KEYS:
159 val = raw.get(key, key in {"github_contents", "github_meta"})
160 if not isinstance(val, bool):
161 raise HostedDashboardConfigError(f"hosted_dashboard.sources.{key} must be a boolean")
162 values[key] = val
163 return HostedSourcesConfig(
164 github_contents=values["github_contents"],
165 github_meta=values["github_meta"],
166 github_checks_advisory=values["github_checks_advisory"],
167 musehub_read=values["musehub_read"],
168 )