"""Environment helper (D-108, REQ-159, REQ-164, REQ-330). During the Nova rebrand transition window (P2–P4), `get_env` read `NOVA_*` preferred with the legacy `ACDL_*` name as the fallback. **P5 (REQ-164) removed the fallback** — `get_env` now reads `NOVA_*` only. `get_env(name, default=None)` resolves `NOVA_`, then returns `default` if unset. Direct-read paths that bypass this helper (the `.env.secrets` shell export in `scripts/run_platform.sh` and the Python parser in `core/regression_verify.py`) were updated to NOVA-only in P5 (the G-106 dual-read contract was retired with the fallback). P2 (REQ-330): `synthesize_local_env(contract_path, environment)` produces a purely synthetic local env dict (account_id placeholder, region "local", no real AWS resources) from a contract YAML. Mirrors the shape of core/environments/*.json (validates against schemas/environment.schema.json) so `nova apply --local` can run the contract resolver + Terraform adapter without provisioning cloud resources. This is the local-tier counterpart of core/onboarding.py:generate_env_file() (the request-path binding generator). """ from __future__ import annotations import os from pathlib import Path from typing import Any, Dict, Optional import yaml __all__ = ["get_env", "synthesize_local_env"] def get_env(name: str, default: Optional[str] = None) -> Optional[str]: """Resolve a config value from the `NOVA_*` environment. `name` is the bare key WITHOUT the prefix (e.g. ``"AWS_ACCOUNT_ID"``). Returns ``NOVA_`` if set and non-empty, else ``default``. """ val = os.environ.get(f"NOVA_{name}") if val: return val return default # Default confidence thresholds per environment name (mirrors the schema # description: dev 0.50, qa 0.75, prod 0.90, dr 0.95). Used by # synthesize_local_env so the synthetic env matches the real env semantics. _DEFAULT_THRESHOLDS: Dict[str, float] = { "dev": 0.50, "qa": 0.75, "prod": 0.90, "dr": 0.95, } def synthesize_local_env( contract_path: str, environment: Optional[str] = None, ) -> Dict[str, Any]: """Synthesize a local env dict from a contract YAML (REQ-330). Reads the contract YAML (``yaml.safe_load``), derives a placeholder environment binding that ``nova apply --local`` can use WITHOUT provisioning real AWS resources. The produced dict: - ``name`` — the environment name (from the arg or the contract's ``environment`` field, defaulting to ``"dev"``). - ``account_id`` — ``"000000000000"`` (the schema-allowed placeholder for an unbound environment; real account id filled by the platform). - ``region`` — ``"local"`` (the local-tier sentinel; never a real AWS region). - ``state_backend`` — ``{bucket: "local-tfstate", lock_table: "local-locks"}`` (local state; LocalS3StateBackend rewrites the terraform backend to ``backend "local"`` using the stack name as the state path, so no S3 bucket is used). - ``network`` — a local RFC1918 CIDR + a single fake AZ. - ``runner_role_arn`` — a placeholder ARN for the local tier. - ``autonomy`` — ``"full"`` (the local tier is autonomous). - ``confidence_threshold`` — the per-env default (0.50 for dev). The dict mirrors the shape of ``core/environments/*.json`` and validates against ``schemas/environment.schema.json``. No cloud provisioning occurs — purely synthetic. Args: contract_path: Path to the contract YAML file. environment: Optional environment name override (defaults to the contract's ``environment`` field, or ``"dev"``). Returns: The synthetic local env dict. """ contract_path_obj = Path(contract_path) contract: Dict[str, Any] = {} if contract_path_obj.is_file(): with open(contract_path_obj) as fh: contract = yaml.safe_load(fh) or {} env_name = environment or contract.get("environment", "dev") stack_name = contract.get("id", env_name) threshold = _DEFAULT_THRESHOLDS.get(env_name, 0.50) return { "name": env_name, "description": ( f"Synthetic local-tier environment for contract '{stack_name}' " f"(environment={env_name}). No real AWS resources — generated " f"by core.env.synthesize_local_env (REQ-330) for nova apply --local." ), "account_id": "000000000000", "region": "local", "state_backend": { "bucket": "local-tfstate", "lock_table": "local-locks", }, "network": { "vpc_cidr": "10.250.0.0/16", "azs": ["local-a"], }, "runner_role_arn": "arn:aws:iam::000000000000:role/local-runner", "autonomy": "full", "confidence_threshold": threshold, }