Coverage for cli / managed_config.py: 100.00%

287 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-09-14 22:07 +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, unique, non-empty — and together with the effective 

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

126 """ 

127 validated_regional_deployment_regions(list(candidate)) 

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

129 if not isinstance(container, dict): 

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

131 scalars = tuple( 

132 container.get(scalar_key, _DEFAULT_SCALAR_REGION) 

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

134 ) 

135 validated_deployment_partition((*scalars, *candidate)) 

136 

137 

138@dataclass(frozen=True) 

139class ManagedScalarKey: 

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

141 

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

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

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

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

146 """ 

147 

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

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

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

151 description: str 

152 default: str 

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

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

155 

156 

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

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

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

160 if not isinstance(container, dict): 

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

162 return { 

163 scalar_key: container.get(scalar_key, _DEFAULT_SCALAR_REGION) 

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

165 } 

166 

167 

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

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

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

171 if not isinstance(container, dict): 

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

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

174 if not isinstance(regional, list): 

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

176 return tuple(regional) 

177 

178 

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

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

181 

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

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

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

185 enforces, applied to the result. 

186 """ 

187 

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

189 scalars = _effective_deployment_scalars(document) 

190 scalars[role] = candidate 

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

192 

193 return _validate 

194 

195 

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

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

198 

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

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

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

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

203 """ 

204 

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

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

207 if not candidate.strip(): 

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

209 if candidate != candidate.strip(): 

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

211 

212 return _validate 

213 

214 

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

216 

217 

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

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

220 if candidate not in _CODEX_REASONING_EFFORTS: 

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

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

223 

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

225 if bedrock is None: 

226 return 

227 if not isinstance(bedrock, dict): 

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

229 codex = bedrock.get("codex") 

230 if codex is None: 

231 return 

232 if not isinstance(codex, dict): 

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

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

235 if unexpected: 

236 raise ValueError( 

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

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

239 ) 

240 

241 

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

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

244REGIONAL_DEPLOYMENT_REGIONS = ManagedListKey( 

245 key_id="deployment_regions.regional", 

246 container="deployment_regions", 

247 leaf="regional", 

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

249 default=_DEFAULT_REGIONAL, 

250 validate_result=_validate_regional_result, 

251) 

252 

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

254DEPLOYMENT_REGION_SCALARS: dict[str, ManagedScalarKey] = { 

255 role: ManagedScalarKey( 

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

257 container="deployment_regions", 

258 leaf=role, 

259 description=description, 

260 default=_DEFAULT_SCALAR_REGION, 

261 validate_result=_scalar_region_validator(role), 

262 ) 

263 for role, description in ( 

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

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

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

267 ) 

268} 

269 

270MISSION_DEFAULT_MODEL = ManagedScalarKey( 

271 key_id="bedrock.mission_default_model_id", 

272 container="bedrock", 

273 leaf="mission_default_model_id", 

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

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

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

277) 

278 

279CAPACITY_ADVISOR_DEFAULT_MODEL = ManagedScalarKey( 

280 key_id="bedrock.capacity_advisor_default_model_id", 

281 container="bedrock", 

282 leaf="capacity_advisor_default_model_id", 

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

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

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

286) 

287 

288CLAUDE_CODE_DEFAULT_MODEL = ManagedScalarKey( 

289 key_id="bedrock.claude_code_default_model_id", 

290 container="bedrock", 

291 leaf="claude_code_default_model_id", 

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

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

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

295) 

296 

297CODEX_DEFAULT_MODEL = ManagedScalarKey( 

298 key_id="bedrock.codex_default_model_id", 

299 container="bedrock", 

300 leaf="codex_default_model_id", 

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

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

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

304) 

305 

306CODEX_REASONING_EFFORT = ManagedScalarKey( 

307 key_id="bedrock.codex.reasoning_effort", 

308 container="bedrock", 

309 nested=("codex",), 

310 leaf="reasoning_effort", 

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

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

313 validate_result=_codex_reasoning_effort_validator, 

314) 

315 

316 

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

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

319 if config_path is not None: 

320 path = Path(config_path) 

321 if not path.is_file(): 

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

323 return path 

324 found = _find_cdk_json() 

325 if found is None: 

326 raise ManagedConfigError( 

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

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

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

330 "deployment config." 

331 ) 

332 return found 

333 

334 

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

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

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

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

339 if parent_writable and file_writable: 

340 return 

341 raise ManagedConfigError( 

342 f"{path} is not writable" 

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

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

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

346 "at the deployment config you own." 

347 ) 

348 

349 

350@contextmanager 

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

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

353 try: 

354 with _shared_config_mutation_lock(path): 

355 yield 

356 except ConfigMutationLockError as exc: 

357 raise ManagedConfigError(str(exc)) from exc 

358 

359 

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

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

362 raw = path.read_bytes() 

363 try: 

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

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

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

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

368 raise ManagedConfigError( 

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

370 "refusing to edit it" 

371 ) 

372 return document, raw 

373 

374 

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

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

377 

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

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

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

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

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

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

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

385 """ 

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

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

388 serialized += b"\n" 

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

390 

391 

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

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

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

395 if container is None: 

396 return key.default 

397 if not isinstance(container, dict): 

398 raise ManagedConfigError( 

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

400 ) 

401 values = container.get(key.leaf) 

402 if values is None: 

403 return key.default 

404 if not isinstance(values, list): 

405 raise ManagedConfigError( 

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

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

408 ) 

409 return tuple(values) 

410 

411 

412def _apply( 

413 key: ManagedListKey, 

414 action: str, 

415 value: str, 

416 config_path: Path | str | None, 

417) -> ChangeReport: 

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

419 path = _resolve_config_path(config_path) 

420 with _config_mutation_lock(path): 

421 document, raw = _load_document(path) 

422 old = _current_values(document, key) 

423 

424 if action == "add": 

425 changed = value not in old 

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

427 else: 

428 changed = value in old 

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

430 

431 if not changed: 

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

433 logger.info( 

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

435 key.key_id, 

436 action, 

437 value, 

438 path, 

439 ) 

440 return report 

441 

442 try: 

443 key.validate_result(document, candidate) 

444 except ValueError as exc: 

445 logger.warning( 

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

447 key.key_id, 

448 action, 

449 value, 

450 path, 

451 exc, 

452 ) 

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

454 

455 _require_writable(path) 

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

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

458 # the file by an unrelated edit. 

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

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

461 _write_document(path, document, raw) 

462 

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

464 logger.info( 

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

466 key.key_id, 

467 action, 

468 value, 

469 list(old), 

470 list(candidate), 

471 path, 

472 ) 

473 return report 

474 

475 

476def managed_list_add( 

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

478) -> ChangeReport: 

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

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

481 

482 

483def managed_list_remove( 

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

485) -> ChangeReport: 

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

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

488 

489 

490def _scalar_container( 

491 document: dict[str, Any], 

492 key: ManagedScalarKey, 

493 *, 

494 materialize: bool = False, 

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

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

497 context = document["context"] 

498 container = context.get(key.container) 

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

500 if container is None: 

501 if not materialize: 

502 return None 

503 container = {} 

504 context[key.container] = container 

505 if not isinstance(container, dict): 

506 path = ".".join(path_parts) 

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

508 

509 for segment in key.nested: 

510 path_parts.append(segment) 

511 child = container.get(segment) 

512 if child is None: 

513 if not materialize: 

514 return None 

515 child = {} 

516 container[segment] = child 

517 if not isinstance(child, dict): 

518 path = ".".join(path_parts) 

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

520 container = child 

521 return container 

522 

523 

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

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

526 container = _scalar_container(document, key) 

527 if container is None: 

528 return key.default 

529 value = container.get(key.leaf) 

530 if value is None: 

531 return key.default 

532 if not isinstance(value, str): 

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

534 raise ManagedConfigError( 

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

536 ) 

537 return value 

538 

539 

540def managed_scalar_set( 

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

542) -> ChangeReport: 

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

544 path = _resolve_config_path(config_path) 

545 with _config_mutation_lock(path): 

546 document, raw = _load_document(path) 

547 old = _current_scalar(document, key) 

548 

549 try: 

550 key.validate_result(document, value) 

551 except ValueError as exc: 

552 logger.warning( 

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

554 key.key_id, 

555 value, 

556 path, 

557 exc, 

558 ) 

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

560 

561 if value == old: 

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

563 logger.info( 

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

565 key.key_id, 

566 value, 

567 path, 

568 ) 

569 return report 

570 

571 _require_writable(path) 

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

573 # retain their current state / reader defaults. 

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

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

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

577 container[key.leaf] = value 

578 _write_document(path, document, raw) 

579 

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

581 logger.info( 

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

583 key.key_id, 

584 value, 

585 old, 

586 value, 

587 path, 

588 ) 

589 return report 

590 

591 

592# --------------------------------------------------------------------------- 

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

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

595# --------------------------------------------------------------------------- 

596 

597 

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

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

600 

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

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

603 ``partition_error`` carries the validator message. 

604 """ 

605 path = _resolve_config_path(config_path) 

606 document, _ = _load_document(path) 

607 container_key = REGIONAL_DEPLOYMENT_REGIONS.container 

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

609 if container is None: 

610 container = {} 

611 if not isinstance(container, dict): 

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

613 regional = _current_values(document, REGIONAL_DEPLOYMENT_REGIONS) 

614 status: dict[str, Any] = { 

615 "config_path": str(path), 

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

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

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

619 "regional": list(regional), 

620 } 

621 try: 

622 _validate_regional_result(document, regional) 

623 status["partition"] = validated_deployment_partition( 

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

625 ) 

626 except ValueError as exc: 

627 status["partition"] = None 

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

629 return status 

630 

631 

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

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

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

635 

636 

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

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

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

640 

641 

642def set_deployment_region_role( 

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

644) -> ChangeReport: 

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

646 key = DEPLOYMENT_REGION_SCALARS.get(role) 

647 if key is None: 

648 raise ManagedConfigError( 

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

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

651 ) 

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

653 

654 

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

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

657 path = _resolve_config_path(config_path) 

658 document, _ = _load_document(path) 

659 return { 

660 "config_path": str(path), 

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

662 "capacity_advisor_default_model_id": _current_scalar( 

663 document, CAPACITY_ADVISOR_DEFAULT_MODEL 

664 ), 

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

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

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

668 } 

669 

670 

671def set_mission_default_model( 

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

673) -> ChangeReport: 

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

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

676 

677 

678def set_capacity_advisor_default_model( 

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

680) -> ChangeReport: 

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

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

683 

684 

685def set_claude_code_default_model( 

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

687) -> ChangeReport: 

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

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

690 

691 

692def set_codex_default_model( 

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

694) -> ChangeReport: 

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

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

697 

698 

699def set_codex_reasoning_effort( 

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

701) -> ChangeReport: 

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

703 return managed_scalar_set( 

704 CODEX_REASONING_EFFORT, 

705 reasoning_effort, 

706 config_path=config_path, 

707 )