Coverage for cli / managed_config.py: 100.00%

290 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-10-28 16:19 +0000

1"""Managed deployment-config engine: validated, atomic, audited cdk.json edits. 

2 

3This module is the categorical answer to "add a CLI/MCP toggle for cdk.json 

4knob X" requests (issue #221). Instead of re-implementing read/validate/write 

5logic per knob, each externally manageable key registers a :class:`ManagedListKey` 

6(set-semantics list) or :class:`ManagedScalarKey` (single string value) and 

7every mutation flows through one engine that guarantees: 

8 

9- **Resolution**: the target is the same ``cdk.json`` the CDK CLI would use 

10 (current directory upward), or an explicit caller-supplied path. Installed 

11 (``uvx`` / ``pip``) distributions resolve to read-only package data — the 

12 engine refuses those with an actionable message instead of half-working. 

13- **Validation of the result, not the starting state**: an edit is accepted 

14 iff the *resulting* configuration passes the same validators the CDK app 

15 applies at synth time (``gco/stacks/constants.py``). This deliberately 

16 allows repairing an already-broken config (e.g. removing a typo'd Region). 

17- **Idempotency**: re-adding a present value or removing an absent one is a 

18 reported no-op — no bytes are written, no timestamps churn. 

19- **Atomicity**: writes go through the same tmp-file + ``os.replace`` dance 

20 as the feature toggles in ``cli/stacks.py``, preserving file mode and the 

21 original trailing-newline state, so a crash can never leave a torn file. 

22- **Auditability**: every mutation attempt logs a structured line on the 

23 ``gco.cli.managed_config`` logger; MCP exposure adds ``@audit_logged`` on 

24 top of that. 

25 

26Comment keys (``_comment_*``) and key order in ``cdk.json`` are preserved 

27because the engine round-trips the whole document with ``json.load`` / 

28``json.dumps(indent=2)`` exactly like the existing feature-toggle writers. 

29""" 

30 

31from __future__ import annotations 

32 

33import json 

34import logging 

35import os 

36import stat 

37from collections.abc import Callable, Iterator 

38from contextlib import contextmanager 

39from dataclasses import dataclass 

40from pathlib import Path 

41from typing import Any 

42 

43from gco.stacks.constants import ( 

44 validated_deployment_partition, 

45 validated_regional_deployment_regions, 

46) 

47 

48from .stacks import ( 

49 ConfigMutationLockError, 

50 _atomic_write_bytes, 

51 _find_cdk_json, 

52) 

53from .stacks import ( 

54 _config_mutation_lock as _shared_config_mutation_lock, 

55) 

56 

57logger = logging.getLogger("gco.cli.managed_config") 

58 

59# Effective defaults mirror the reader contract in 

60# ``gco/config/config_loader.py::get_deployment_regions`` — validation of a 

61# candidate result must see the same effective document the CDK app will. 

62_DEFAULT_SCALAR_REGION = "us-east-2" 

63_DEFAULT_REGIONAL = ("us-east-1",) 

64 

65 

66class ManagedConfigError(RuntimeError): 

67 """A managed cdk.json edit was refused; the message says how to proceed.""" 

68 

69 

70@dataclass(frozen=True) 

71class ChangeReport: 

72 """Uniform result of one managed mutation (including reported no-ops).""" 

73 

74 key_id: str 

75 action: str # "add" | "remove" | "set" 

76 value: str 

77 changed: bool 

78 old: tuple[str, ...] | str 

79 new: tuple[str, ...] | str 

80 config_path: Path 

81 

82 @staticmethod 

83 def _render(side: tuple[str, ...] | str) -> str: 

84 return repr(list(side)) if isinstance(side, tuple) else repr(side) 

85 

86 def summary(self) -> str: 

87 """One human line suitable for CLI output and audit trails.""" 

88 if not self.changed: 

89 state = { 

90 "add": "already present", 

91 "remove": "not present", 

92 "set": "already the value", 

93 }[self.action] 

94 return f"{self.key_id}: no change ({self.value!r} {state})" 

95 return ( 

96 f"{self.key_id}: {self.action} {self.value!r} " 

97 f"({self._render(self.old)} -> {self._render(self.new)}) in {self.config_path}" 

98 ) 

99 

100 

101@dataclass(frozen=True) 

102class ManagedListKey: 

103 """Registry entry for a cdk.json context list managed with set semantics. 

104 

105 ``validate_result`` receives the full parsed cdk.json document and the 

106 candidate value the list would hold after the edit; it must raise 

107 ``ValueError`` to reject. Validating in document context lets a key 

108 enforce cross-key invariants (the regional-Regions key checks the single 

109 partition constraint against the global/API/monitoring scalars). 

110 """ 

111 

112 key_id: str # dotted id, e.g. "deployment_regions.regional" 

113 container: str # context child object, e.g. "deployment_regions" 

114 leaf: str # list key inside the container, e.g. "regional" 

115 description: str 

116 default: tuple[str, ...] 

117 validate_result: Callable[[dict[str, Any], tuple[str, ...]], None] 

118 

119 

120def _validate_regional_result(document: dict[str, Any], candidate: tuple[str, ...]) -> None: 

121 """Reject a regional-Regions candidate the CDK app would refuse at synth. 

122 

123 Applies the exact synth-time validators: every entry must be an SDK-known 

124 CloudFormation Region and unique — and together with the effective 

125 global/api_gateway/monitoring scalars must resolve to one AWS partition. 

126 An empty candidate is valid: it describes a control-plane-only deployment 

127 (global, API Gateway, and monitoring stacks with no workload cluster). 

128 """ 

129 validated_regional_deployment_regions(list(candidate)) 

130 container = document.get("context", {}).get("deployment_regions", {}) 

131 if not isinstance(container, dict): 

132 raise ValueError("context.deployment_regions must be a JSON object") 

133 scalars = tuple( 

134 container.get(scalar_key, _DEFAULT_SCALAR_REGION) 

135 for scalar_key in ("global", "api_gateway", "monitoring") 

136 ) 

137 validated_deployment_partition((*scalars, *candidate)) 

138 

139 

140@dataclass(frozen=True) 

141class ManagedScalarKey: 

142 """Registry entry for a single-valued cdk.json context string. 

143 

144 Same contract as :class:`ManagedListKey` with scalar semantics: 

145 ``validate_result`` receives the full parsed document and the candidate 

146 string the key would hold after the edit, raising ``ValueError`` to 

147 reject. Setting the current value is a reported no-op. 

148 """ 

149 

150 key_id: str # dotted id, e.g. "deployment_regions.global" 

151 container: str # context child object, e.g. "deployment_regions" 

152 leaf: str # string key inside the container, e.g. "global" 

153 description: str 

154 default: str 

155 validate_result: Callable[[dict[str, Any], str], None] 

156 nested: tuple[str, ...] = () 

157 

158 

159def _effective_deployment_scalars(document: dict[str, Any]) -> dict[str, str]: 

160 """Return the effective global/api_gateway/monitoring scalar Regions.""" 

161 container = document.get("context", {}).get("deployment_regions", {}) 

162 if not isinstance(container, dict): 

163 raise ValueError("context.deployment_regions must be a JSON object") 

164 return { 

165 scalar_key: container.get(scalar_key, _DEFAULT_SCALAR_REGION) 

166 for scalar_key in ("global", "api_gateway", "monitoring") 

167 } 

168 

169 

170def _effective_regional(document: dict[str, Any]) -> tuple[str, ...]: 

171 """Return the effective workload-Region list (default when absent).""" 

172 container = document.get("context", {}).get("deployment_regions", {}) 

173 if not isinstance(container, dict): 

174 raise ValueError("context.deployment_regions must be a JSON object") 

175 regional = container.get("regional", list(_DEFAULT_REGIONAL)) 

176 if not isinstance(regional, list): 

177 raise ValueError("context.deployment_regions.regional must be a JSON array") 

178 return tuple(regional) 

179 

180 

181def _scalar_region_validator(role: str) -> Callable[[dict[str, Any], str], None]: 

182 """Build a validator for one deployment-region scalar (``role``). 

183 

184 The candidate must be an SDK-known CloudFormation Region and the whole 

185 resulting topology (candidate + the other scalars + the workload list) 

186 must still resolve to one AWS partition — the same constraint synth 

187 enforces, applied to the result. 

188 """ 

189 

190 def _validate(document: dict[str, Any], candidate: str) -> None: 

191 scalars = _effective_deployment_scalars(document) 

192 scalars[role] = candidate 

193 validated_deployment_partition((*scalars.values(), *_effective_regional(document))) 

194 

195 return _validate 

196 

197 

198def _bedrock_model_id_validator(key_id: str) -> Callable[[dict[str, Any], str], None]: 

199 """Mirror the reader contract in ``gco/bedrock.py``: a non-empty string. 

200 

201 Model/inference-profile IDs are free-form by design (custom profiles, 

202 marketplace models); the runtime readers only require a non-empty 

203 string, so requiring more here would reject valid configurations. One 

204 factory serves both Bedrock model knobs so their contracts cannot drift. 

205 """ 

206 

207 def _validate(document: dict[str, Any], candidate: str) -> None: 

208 del document # no cross-key invariants for these knobs 

209 if not candidate.strip(): 

210 raise ValueError(f"{key_id} must be a non-empty string") 

211 if candidate != candidate.strip(): 

212 raise ValueError(f"{key_id} must not have leading/trailing whitespace") 

213 

214 return _validate 

215 

216 

217_CODEX_REASONING_EFFORTS = frozenset({"minimal", "low", "medium", "high", "xhigh"}) 

218 

219 

220def _codex_reasoning_effort_validator(document: dict[str, Any], candidate: str) -> None: 

221 """Mirror the canonical Codex effort/object contract in ``gco.bedrock``.""" 

222 if candidate not in _CODEX_REASONING_EFFORTS: 

223 supported = ", ".join(sorted(_CODEX_REASONING_EFFORTS)) 

224 raise ValueError("bedrock.codex.reasoning_effort must be one of " + supported) 

225 

226 bedrock = document.get("context", {}).get("bedrock") 

227 if bedrock is None: 

228 return 

229 if not isinstance(bedrock, dict): 

230 raise ValueError("context.bedrock must be a JSON object") 

231 codex = bedrock.get("codex") 

232 if codex is None: 

233 return 

234 if not isinstance(codex, dict): 

235 raise ValueError("context.bedrock.codex must be a JSON object") 

236 unexpected = sorted(set(codex) - {"reasoning_effort"}) 

237 if unexpected: 

238 raise ValueError( 

239 "context.bedrock.codex must contain only 'reasoning_effort'; " 

240 f"unexpected keys: {', '.join(unexpected)}" 

241 ) 

242 

243 

244#: The managed-key registry. New knobs register here instead of growing 

245#: bespoke read/validate/write code paths. 

246REGIONAL_DEPLOYMENT_REGIONS = ManagedListKey( 

247 key_id="deployment_regions.regional", 

248 container="deployment_regions", 

249 leaf="regional", 

250 description="Workload Regions that receive an EKS regional stack", 

251 default=_DEFAULT_REGIONAL, 

252 validate_result=_validate_regional_result, 

253) 

254 

255#: The three control-plane region scalars, addressable by role name. 

256DEPLOYMENT_REGION_SCALARS: dict[str, ManagedScalarKey] = { 

257 role: ManagedScalarKey( 

258 key_id=f"deployment_regions.{role}", 

259 container="deployment_regions", 

260 leaf=role, 

261 description=description, 

262 default=_DEFAULT_SCALAR_REGION, 

263 validate_result=_scalar_region_validator(role), 

264 ) 

265 for role, description in ( 

266 ("global", "Region hosting partition-wide ECR/S3/DynamoDB and the SSM registry"), 

267 ("api_gateway", "Region hosting the API Gateway stack"), 

268 ("monitoring", "Region hosting the monitoring stack"), 

269 ) 

270} 

271 

272MISSION_DEFAULT_MODEL = ManagedScalarKey( 

273 key_id="bedrock.mission_default_model_id", 

274 container="bedrock", 

275 leaf="mission_default_model_id", 

276 description="Bedrock model/inference-profile ID Mission sampling uses by default", 

277 default="", # the reader has no fallback: it requires the key when consulted 

278 validate_result=_bedrock_model_id_validator("bedrock.mission_default_model_id"), 

279) 

280 

281CAPACITY_ADVISOR_DEFAULT_MODEL = ManagedScalarKey( 

282 key_id="bedrock.capacity_advisor_default_model_id", 

283 container="bedrock", 

284 leaf="capacity_advisor_default_model_id", 

285 description="Bedrock model/inference-profile ID the capacity advisor uses by default", 

286 default="", # the reader has no fallback: it requires the key when consulted 

287 validate_result=_bedrock_model_id_validator("bedrock.capacity_advisor_default_model_id"), 

288) 

289 

290CLAUDE_CODE_DEFAULT_MODEL = ManagedScalarKey( 

291 key_id="bedrock.claude_code_default_model_id", 

292 container="bedrock", 

293 leaf="claude_code_default_model_id", 

294 description="Bedrock model/inference-profile ID gco autopilot hands to Claude Code", 

295 default="", # the reader has no fallback: it requires the key when consulted 

296 validate_result=_bedrock_model_id_validator("bedrock.claude_code_default_model_id"), 

297) 

298 

299CODEX_DEFAULT_MODEL = ManagedScalarKey( 

300 key_id="bedrock.codex_default_model_id", 

301 container="bedrock", 

302 leaf="codex_default_model_id", 

303 description="Bedrock model/inference-profile ID gco autopilot hands to Codex", 

304 default="", # the reader has no fallback: it requires the key when consulted 

305 validate_result=_bedrock_model_id_validator("bedrock.codex_default_model_id"), 

306) 

307 

308CODEX_REASONING_EFFORT = ManagedScalarKey( 

309 key_id="bedrock.codex.reasoning_effort", 

310 container="bedrock", 

311 nested=("codex",), 

312 leaf="reasoning_effort", 

313 description="Canonical reasoning effort for the default Codex model", 

314 default="", # the reader has no fallback: it requires the key when consulted 

315 validate_result=_codex_reasoning_effort_validator, 

316) 

317 

318OPENCODE_DEFAULT_MODEL = ManagedScalarKey( 

319 key_id="bedrock.opencode_default_model_id", 

320 container="bedrock", 

321 leaf="opencode_default_model_id", 

322 description="Bedrock model/inference-profile ID gco autopilot hands to OpenCode", 

323 default="", # the reader has no fallback: it requires the key when consulted 

324 validate_result=_bedrock_model_id_validator("bedrock.opencode_default_model_id"), 

325) 

326 

327 

328def _resolve_config_path(config_path: Path | str | None) -> Path: 

329 """Return the cdk.json to edit, refusing unusable resolutions early.""" 

330 if config_path is not None: 

331 path = Path(config_path) 

332 if not path.is_file(): 

333 raise ManagedConfigError(f"config path {path} does not exist or is not a file") 

334 return path 

335 found = _find_cdk_json() 

336 if found is None: 

337 raise ManagedConfigError( 

338 "cdk.json not found in the current directory or any parent. " 

339 "Run from a GCO checkout, or pass an explicit --config-path. " 

340 "Installed (uvx/pip) distributions do not carry a writable " 

341 "deployment config." 

342 ) 

343 return found 

344 

345 

346def _require_writable(path: Path) -> None: 

347 """Refuse read-only targets (the installed-mode package-data case).""" 

348 parent_writable = os.access(path.parent, os.W_OK) 

349 file_writable = os.access(path, os.W_OK) 

350 if parent_writable and file_writable: 

351 return 

352 raise ManagedConfigError( 

353 f"{path} is not writable" 

354 f"{'' if parent_writable else ' (directory is read-only)'}. " 

355 "Installed (uvx/pip) distributions expose a read-only cdk.json; " 

356 "run from a writable GCO checkout or pass --config-path pointing " 

357 "at the deployment config you own." 

358 ) 

359 

360 

361@contextmanager 

362def _config_mutation_lock(path: Path) -> Iterator[None]: 

363 """Translate shared-lock failures into this module's public error type.""" 

364 try: 

365 with _shared_config_mutation_lock(path): 

366 yield 

367 except ConfigMutationLockError as exc: 

368 raise ManagedConfigError(str(exc)) from exc 

369 

370 

371def _load_document(path: Path) -> tuple[dict[str, Any], bytes]: 

372 """Parse the target document, keeping raw bytes for faithful re-encoding.""" 

373 raw = path.read_bytes() 

374 try: 

375 document = json.loads(raw.decode("utf-8")) 

376 except (UnicodeDecodeError, json.JSONDecodeError) as exc: 

377 raise ManagedConfigError(f"{path} is not valid JSON: {exc}") from exc 

378 if not isinstance(document, dict) or not isinstance(document.get("context"), dict): 

379 raise ManagedConfigError( 

380 f"{path} does not look like a GCO cdk.json (missing a 'context' object); " 

381 "refusing to edit it" 

382 ) 

383 return document, raw 

384 

385 

386def _write_document(path: Path, document: dict[str, Any], original_raw: bytes) -> None: 

387 """Serialize like the existing feature-toggle writers, atomically. 

388 

389 ``json.dumps(indent=2)`` with insertion order preserves ``_comment_*`` 

390 keys and their placement. ``ensure_ascii=False`` keeps the em dashes and 

391 other non-ASCII characters inside those comments as UTF-8 rather than 

392 rewriting them to ``\\uXXXX`` escapes — without it, adding one Region 

393 rewrites every documented block in the file and buries the real change in 

394 hundreds of lines of encoding churn. The original trailing-newline state is 

395 kept so diffs stay minimal regardless of how the file was last formatted. 

396 """ 

397 serialized = json.dumps(document, indent=2, ensure_ascii=False).encode("utf-8") 

398 if original_raw.endswith(b"\n"): 

399 serialized += b"\n" 

400 _atomic_write_bytes(path, serialized, mode=stat.S_IMODE(path.stat().st_mode)) 

401 

402 

403def _current_values(document: dict[str, Any], key: ManagedListKey) -> tuple[str, ...]: 

404 """Return the configured list, or the effective default when absent.""" 

405 container = document["context"].get(key.container) 

406 if container is None: 

407 return key.default 

408 if not isinstance(container, dict): 

409 raise ManagedConfigError( 

410 f"context.{key.container} must be a JSON object, found {type(container).__name__}" 

411 ) 

412 values = container.get(key.leaf) 

413 if values is None: 

414 return key.default 

415 if not isinstance(values, list): 

416 raise ManagedConfigError( 

417 f"context.{key.container}.{key.leaf} must be a JSON array, " 

418 f"found {type(values).__name__}" 

419 ) 

420 return tuple(values) 

421 

422 

423def _apply( 

424 key: ManagedListKey, 

425 action: str, 

426 value: str, 

427 config_path: Path | str | None, 

428) -> ChangeReport: 

429 """Shared add/remove core: resolve, lock, load, validate, and write.""" 

430 path = _resolve_config_path(config_path) 

431 with _config_mutation_lock(path): 

432 document, raw = _load_document(path) 

433 old = _current_values(document, key) 

434 

435 if action == "add": 

436 changed = value not in old 

437 candidate = (*old, value) if changed else old 

438 else: 

439 changed = value in old 

440 candidate = tuple(entry for entry in old if entry != value) if changed else old 

441 

442 if not changed: 

443 report = ChangeReport(key.key_id, action, value, False, old, old, path) 

444 logger.info( 

445 "managed-config no-op: key=%s action=%s value=%s path=%s", 

446 key.key_id, 

447 action, 

448 value, 

449 path, 

450 ) 

451 return report 

452 

453 try: 

454 key.validate_result(document, candidate) 

455 except ValueError as exc: 

456 logger.warning( 

457 "managed-config refused: key=%s action=%s value=%s path=%s reason=%s", 

458 key.key_id, 

459 action, 

460 value, 

461 path, 

462 exc, 

463 ) 

464 raise ManagedConfigError(f"refusing to update {key.key_id}: {exc}") from exc 

465 

466 _require_writable(path) 

467 # Materialize only what this key manages; absent sibling scalars keep 

468 # falling through to the reader defaults instead of being frozen into 

469 # the file by an unrelated edit. 

470 container = document["context"].setdefault(key.container, {}) 

471 container[key.leaf] = list(candidate) 

472 _write_document(path, document, raw) 

473 

474 report = ChangeReport(key.key_id, action, value, True, old, candidate, path) 

475 logger.info( 

476 "managed-config write: key=%s action=%s value=%s old=%s new=%s path=%s", 

477 key.key_id, 

478 action, 

479 value, 

480 list(old), 

481 list(candidate), 

482 path, 

483 ) 

484 return report 

485 

486 

487def managed_list_add( 

488 key: ManagedListKey, value: str, *, config_path: Path | str | None = None 

489) -> ChangeReport: 

490 """Add ``value`` to a managed list if absent; validated and atomic.""" 

491 return _apply(key, "add", value, config_path) 

492 

493 

494def managed_list_remove( 

495 key: ManagedListKey, value: str, *, config_path: Path | str | None = None 

496) -> ChangeReport: 

497 """Remove ``value`` from a managed list if present; validated and atomic.""" 

498 return _apply(key, "remove", value, config_path) 

499 

500 

501def _scalar_container( 

502 document: dict[str, Any], 

503 key: ManagedScalarKey, 

504 *, 

505 materialize: bool = False, 

506) -> dict[str, Any] | None: 

507 """Resolve (and optionally create) a scalar key's nested object path.""" 

508 context = document["context"] 

509 container = context.get(key.container) 

510 path_parts = ["context", key.container] 

511 if container is None: 

512 if not materialize: 

513 return None 

514 container = {} 

515 context[key.container] = container 

516 if not isinstance(container, dict): 

517 path = ".".join(path_parts) 

518 raise ManagedConfigError(f"{path} must be a JSON object, found {type(container).__name__}") 

519 

520 for segment in key.nested: 

521 path_parts.append(segment) 

522 child = container.get(segment) 

523 if child is None: 

524 if not materialize: 

525 return None 

526 child = {} 

527 container[segment] = child 

528 if not isinstance(child, dict): 

529 path = ".".join(path_parts) 

530 raise ManagedConfigError(f"{path} must be a JSON object, found {type(child).__name__}") 

531 container = child 

532 return container 

533 

534 

535def _current_scalar(document: dict[str, Any], key: ManagedScalarKey) -> str: 

536 """Return the configured scalar, or the effective default when absent.""" 

537 container = _scalar_container(document, key) 

538 if container is None: 

539 return key.default 

540 value = container.get(key.leaf) 

541 if value is None: 

542 return key.default 

543 if not isinstance(value, str): 

544 dotted_path = ".".join(("context", key.container, *key.nested, key.leaf)) 

545 raise ManagedConfigError( 

546 f"{dotted_path} must be a JSON string, found {type(value).__name__}" 

547 ) 

548 return value 

549 

550 

551def managed_scalar_set( 

552 key: ManagedScalarKey, value: str, *, config_path: Path | str | None = None 

553) -> ChangeReport: 

554 """Set a managed scalar; validated, atomic, and a no-op when unchanged.""" 

555 path = _resolve_config_path(config_path) 

556 with _config_mutation_lock(path): 

557 document, raw = _load_document(path) 

558 old = _current_scalar(document, key) 

559 

560 try: 

561 key.validate_result(document, value) 

562 except ValueError as exc: 

563 logger.warning( 

564 "managed-config refused: key=%s action=set value=%s path=%s reason=%s", 

565 key.key_id, 

566 value, 

567 path, 

568 exc, 

569 ) 

570 raise ManagedConfigError(f"refusing to update {key.key_id}: {exc}") from exc 

571 

572 if value == old: 

573 report = ChangeReport(key.key_id, "set", value, False, old, old, path) 

574 logger.info( 

575 "managed-config no-op: key=%s action=set value=%s path=%s", 

576 key.key_id, 

577 value, 

578 path, 

579 ) 

580 return report 

581 

582 _require_writable(path) 

583 # Materialize only the target path; all outer and sibling settings 

584 # retain their current state / reader defaults. 

585 container = _scalar_container(document, key, materialize=True) 

586 if container is None: # pragma: no cover - materialize guarantees an object 

587 raise ManagedConfigError(f"unable to materialize {key.key_id}") 

588 container[key.leaf] = value 

589 _write_document(path, document, raw) 

590 

591 report = ChangeReport(key.key_id, "set", value, True, old, value, path) 

592 logger.info( 

593 "managed-config write: key=%s action=set value=%s old=%s new=%s path=%s", 

594 key.key_id, 

595 value, 

596 old, 

597 value, 

598 path, 

599 ) 

600 return report 

601 

602 

603# --------------------------------------------------------------------------- 

604# Deployment-region veneers (domain-named entry points used by the CLI; the 

605# MCP tools shell to the CLI commands, matching every other gated tool). 

606# --------------------------------------------------------------------------- 

607 

608 

609def get_deployment_regions_status(*, config_path: Path | str | None = None) -> dict[str, Any]: 

610 """Return the effective deployment-region topology plus its partition. 

611 

612 Works on broken configurations too (this is the diagnosis entry point): 

613 when the topology fails validation, ``partition`` is ``None`` and 

614 ``partition_error`` carries the validator message. 

615 """ 

616 path = _resolve_config_path(config_path) 

617 document, _ = _load_document(path) 

618 container_key = REGIONAL_DEPLOYMENT_REGIONS.container 

619 container = document["context"].get(container_key) 

620 if container is None: 

621 container = {} 

622 if not isinstance(container, dict): 

623 raise ManagedConfigError(f"context.{container_key} must be a JSON object") 

624 regional = _current_values(document, REGIONAL_DEPLOYMENT_REGIONS) 

625 status: dict[str, Any] = { 

626 "config_path": str(path), 

627 "global": container.get("global", _DEFAULT_SCALAR_REGION), 

628 "api_gateway": container.get("api_gateway", _DEFAULT_SCALAR_REGION), 

629 "monitoring": container.get("monitoring", _DEFAULT_SCALAR_REGION), 

630 "regional": list(regional), 

631 } 

632 try: 

633 _validate_regional_result(document, regional) 

634 status["partition"] = validated_deployment_partition( 

635 (status["global"], status["api_gateway"], status["monitoring"], *regional) 

636 ) 

637 except ValueError as exc: 

638 status["partition"] = None 

639 status["partition_error"] = str(exc) 

640 return status 

641 

642 

643def add_deployment_region(region: str, *, config_path: Path | str | None = None) -> ChangeReport: 

644 """Add a workload Region to ``deployment_regions.regional``.""" 

645 return managed_list_add(REGIONAL_DEPLOYMENT_REGIONS, region, config_path=config_path) 

646 

647 

648def remove_deployment_region(region: str, *, config_path: Path | str | None = None) -> ChangeReport: 

649 """Remove a workload Region from ``deployment_regions.regional``.""" 

650 return managed_list_remove(REGIONAL_DEPLOYMENT_REGIONS, region, config_path=config_path) 

651 

652 

653def set_deployment_region_role( 

654 role: str, region: str, *, config_path: Path | str | None = None 

655) -> ChangeReport: 

656 """Set one control-plane region scalar (``global``/``api_gateway``/``monitoring``).""" 

657 key = DEPLOYMENT_REGION_SCALARS.get(role) 

658 if key is None: 

659 raise ManagedConfigError( 

660 f"unknown deployment-region role {role!r}; " 

661 f"expected one of {sorted(DEPLOYMENT_REGION_SCALARS)}" 

662 ) 

663 return managed_scalar_set(key, region, config_path=config_path) 

664 

665 

666def get_bedrock_model_status(*, config_path: Path | str | None = None) -> dict[str, Any]: 

667 """Return every managed Bedrock model/reasoning default and its path.""" 

668 path = _resolve_config_path(config_path) 

669 document, _ = _load_document(path) 

670 return { 

671 "config_path": str(path), 

672 "mission_default_model_id": _current_scalar(document, MISSION_DEFAULT_MODEL), 

673 "capacity_advisor_default_model_id": _current_scalar( 

674 document, CAPACITY_ADVISOR_DEFAULT_MODEL 

675 ), 

676 "claude_code_default_model_id": _current_scalar(document, CLAUDE_CODE_DEFAULT_MODEL), 

677 "codex_default_model_id": _current_scalar(document, CODEX_DEFAULT_MODEL), 

678 "codex_reasoning_effort": _current_scalar(document, CODEX_REASONING_EFFORT), 

679 "opencode_default_model_id": _current_scalar(document, OPENCODE_DEFAULT_MODEL), 

680 } 

681 

682 

683def set_mission_default_model( 

684 model_id: str, *, config_path: Path | str | None = None 

685) -> ChangeReport: 

686 """Set ``bedrock.mission_default_model_id`` (Mission sampling model default).""" 

687 return managed_scalar_set(MISSION_DEFAULT_MODEL, model_id, config_path=config_path) 

688 

689 

690def set_capacity_advisor_default_model( 

691 model_id: str, *, config_path: Path | str | None = None 

692) -> ChangeReport: 

693 """Set ``bedrock.capacity_advisor_default_model_id`` (capacity-advisor model default).""" 

694 return managed_scalar_set(CAPACITY_ADVISOR_DEFAULT_MODEL, model_id, config_path=config_path) 

695 

696 

697def set_claude_code_default_model( 

698 model_id: str, *, config_path: Path | str | None = None 

699) -> ChangeReport: 

700 """Set ``bedrock.claude_code_default_model_id`` (Claude session model).""" 

701 return managed_scalar_set(CLAUDE_CODE_DEFAULT_MODEL, model_id, config_path=config_path) 

702 

703 

704def set_codex_default_model( 

705 model_id: str, *, config_path: Path | str | None = None 

706) -> ChangeReport: 

707 """Set ``bedrock.codex_default_model_id`` (Codex session model).""" 

708 return managed_scalar_set(CODEX_DEFAULT_MODEL, model_id, config_path=config_path) 

709 

710 

711def set_codex_reasoning_effort( 

712 reasoning_effort: str, *, config_path: Path | str | None = None 

713) -> ChangeReport: 

714 """Set ``bedrock.codex.reasoning_effort`` for the canonical Codex model.""" 

715 return managed_scalar_set( 

716 CODEX_REASONING_EFFORT, 

717 reasoning_effort, 

718 config_path=config_path, 

719 ) 

720 

721 

722def set_opencode_default_model( 

723 model_id: str, *, config_path: Path | str | None = None 

724) -> ChangeReport: 

725 """Set ``bedrock.opencode_default_model_id`` (OpenCode session model).""" 

726 return managed_scalar_set(OPENCODE_DEFAULT_MODEL, model_id, config_path=config_path)