diff --git a/.ciagent/CHECKPOINT.json b/.ciagent/CHECKPOINT.json index 6e0f399..68dbd1a 100644 --- a/.ciagent/CHECKPOINT.json +++ b/.ciagent/CHECKPOINT.json @@ -1,11 +1,11 @@ { - "phase": 4, + "phase": 5, "stage": "complete", "milestone": "v1.15", - "phase_role": "execution", + "phase_role": "final", "attempts": 0, - "updated_at": "2026-07-30T00:10:00Z", - "milestone_complete": false, - "requirements": ["REQ-163"], + "updated_at": "2026-07-30T00:11:00Z", + "milestone_complete": true, + "requirements": ["REQ-155", "REQ-156", "REQ-157", "REQ-158", "REQ-159", "REQ-160", "REQ-161", "REQ-162", "REQ-163", "REQ-164"], "tag": "v1.15.4" } \ No newline at end of file diff --git a/.ciagent/REQUIREMENTS.md b/.ciagent/REQUIREMENTS.md index 4bdba10..a31c35a 100644 --- a/.ciagent/REQUIREMENTS.md +++ b/.ciagent/REQUIREMENTS.md @@ -828,7 +828,7 @@ IDEATE-01..IDEATE-10, mapped to REQ-155..REQ-164. | REQ-161 | P3 | complete | | REQ-162 | P3 | complete | | REQ-163 | P4 | complete | -| REQ-164 | P5 | pending | +| REQ-164 | P5 | complete | ### Out of Scope (v1.15) - Renaming the real Gitea org/repo (`continuous-intelligence/acdl`) or diff --git a/.ciagent/ROADMAP.md b/.ciagent/ROADMAP.md index 010363e..4214080 100644 --- a/.ciagent/ROADMAP.md +++ b/.ciagent/ROADMAP.md @@ -1435,7 +1435,7 @@ After Phase P21: milestone COMPLETE — `v1.13.24` IS the v1.14 release. --- -## v1.15 (active — Nova Rebrand, tag `v1.15.4`) +## v1.15 (complete — Nova Rebrand, tag `v1.15.4`) A full rebrand from **ACDL** / "Agentic Cloud Delivery Platform" → **Nova** / "The New Dawn of DevSecOps — security as a seamless enabler @@ -1620,7 +1620,7 @@ milestone release). (G-104 binding.) update REQUIREMENTS.md (REQ-155..164 marked complete), ROADMAP.md (v1.15 complete), PROJECT.md. Tag final patch `v1.14.5` (IS the milestone release). Merge `milestone/v1.15-nova` → `main`. -- **Status:** in progress (branch `phase/05-final-review-ship`) +- **Status:** complete (v1.15.4, milestone release) - **Depends on:** [P1-P4] - **Requirements:** REQ-164 - **Success Criteria:** diff --git a/adapters/terraform/policy/custom_rules/nova_tagging.py b/adapters/terraform/policy/custom_rules/nova_tagging.py index 03e4ac5..f46cc86 100644 --- a/adapters/terraform/policy/custom_rules/nova_tagging.py +++ b/adapters/terraform/policy/custom_rules/nova_tagging.py @@ -39,12 +39,11 @@ NON_TAGGABLE_TYPES = ( "aws_internet_gateway", ) -# P3 hard mode (D-109): hard-fail when a taggable resource is missing any -# required nova:* tag, or when a resource carries only legacy acdl:* tag -# keys and no nova:* tags. P2 shipped warn mode (`_WARN_MODE = True`); P3 -# flips to `False` (hard-fail) once terraform emits nova:* and the ABAC -# policy is swapped to nova:*. P5 keeps hard mode and additionally fails -# on any acdl:* tag key present at all. +# P5 hard mode (D-109, REQ-164): `_WARN_MODE = False` (set in P3) AND +# any `acdl:*` tag key present at all is a hard FAIL (P5 tightens from +# P3's "acdl:*-only fails" to "any acdl:* key fails"). The legacy tag +# keys are fully removed from terraform (P3); any remaining `acdl:*` key +# is a rebrand regression. _WARN_MODE = False @@ -71,21 +70,13 @@ class NovaTaggingStandard(BaseResourceCheck): tag_keys = set(tag_block.keys()) elif isinstance(tags, dict): tag_keys = set(tags.keys()) + # P5 (REQ-164): any legacy acdl:* tag key present = hard FAIL. + legacy_present = tag_keys & set(LEGACY_TAGS) + if legacy_present: + return CheckResult.FAILED missing = [t for t in REQUIRED_TAGS if t not in tag_keys] if not missing: return CheckResult.PASSED - # Warn mode (D-109, P2): if the resource carries the legacy acdl:* - # tag keys for every required tag, emit a warning rather than a - # hard fail — existing resources still carry acdl:* until P3. - if _WARN_MODE: - has_all_legacy = all(t in tag_keys for t in LEGACY_TAGS) - if has_all_legacy: - sys.stderr.write( - f"[nova_tagging] WARN: {entity_type} has legacy acdl:* tags " - f"but no nova:* tags (P2 warn mode, D-109). Migrate to " - f"nova:* tags before P5.\n" - ) - return CheckResult.PASSED return CheckResult.FAILED check = NovaTaggingStandard() \ No newline at end of file diff --git a/core/env.py b/core/env.py index afd295e..c9999e1 100644 --- a/core/env.py +++ b/core/env.py @@ -1,19 +1,14 @@ -"""Dual-read environment helper (D-108, REQ-159, G-106). +"""Environment helper (D-108, REQ-159, REQ-164). -During the Nova rebrand transition window (P2–P4), every `NOVA_*` -environment variable is the preferred source, with the legacy `ACDL_*` -name as the fallback. This keeps deployments from breaking while the -keys are rotated across `.env`, `.env.secrets`, Gitea repo secrets, and -operator-managed process environments. +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_` first, then falls -back to `ACDL_`, then returns `default` if neither is set. - -This helper is removed (NOVA-only) in P5 (REQ-164). 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`) mirror this contract inline per the G-106 -binding — see those sites for the dual-read shell/Python forms. +`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). """ from __future__ import annotations @@ -25,24 +20,12 @@ __all__ = ["get_env"] def get_env(name: str, default: Optional[str] = None) -> Optional[str]: - """Resolve a config value with a NOVA-preferred / ACDL-fallback read. + """Resolve a config value from the `NOVA_*` environment. `name` is the bare key WITHOUT the prefix (e.g. ``"AWS_ACCOUNT_ID"``). - The lookup order is: - - 1. ``NOVA_`` (preferred) - 2. ``ACDL_`` (legacy fallback, removed in P5) - 3. ``default`` - - Returns the first value that is present and non-empty, or ``default`` - if neither env var is set. An explicitly-set empty string is treated - as "unset" so an operator cannot accidentally shadow the fallback - with a blank NOVA key. + Returns ``NOVA_`` if set and non-empty, else ``default``. """ - nova_val = os.environ.get(f"NOVA_{name}") - if nova_val: - return nova_val - acdl_val = os.environ.get(f"ACDL_{name}") - if acdl_val: - return acdl_val + val = os.environ.get(f"NOVA_{name}") + if val: + return val return default \ No newline at end of file diff --git a/core/local_emulators.py b/core/local_emulators.py index 5721171..59d9011 100644 --- a/core/local_emulators.py +++ b/core/local_emulators.py @@ -502,6 +502,6 @@ if __name__ == "__main__": # Set both so the dual-read in is_local_tier() finds NOVA_* (preferred); # the ACDL_* alias stays for any unmigrated reader until P5. os.environ["NOVA_LOCAL_TIER"] = "1" - os.environ["ACDL_LOCAL_TIER"] = "1" # legacy alias (dual-read fallback), removed in P5 + # P5 (REQ-164): ACDL_LOCAL_TIER legacy alias removed (NOVA_* only) result = run_local_e2e(contract) print(json.dumps(result, indent=2)) \ No newline at end of file diff --git a/core/regression_verify.py b/core/regression_verify.py index cf3fc64..e475323 100755 --- a/core/regression_verify.py +++ b/core/regression_verify.py @@ -315,13 +315,10 @@ def _load_aws_env() -> Dict[str, str]: continue if "=" in line: k, v = line.split("=", 1) - # G-106 binding: dual-read NOVA_* first, ACDL_* fallback. - # The .env.secrets keys are renamed to NOVA_* in P2; the - # ACDL_* fallback covers operators who haven't rotated - # their local .env.secrets yet. Removed in P5. - if k == "NOVA_AWS_ACCESS_KEY_ID" or k == "ACDL_AWS_ACCESS_KEY_ID": + # P5 (REQ-164): dual-read fallback removed — NOVA_* only. + if k == "NOVA_AWS_ACCESS_KEY_ID": env["AWS_ACCESS_KEY_ID"] = v - elif k == "NOVA_AWS_SECRET_ACCESS_KEY" or k == "ACDL_AWS_SECRET_ACCESS_KEY": + elif k == "NOVA_AWS_SECRET_ACCESS_KEY": env["AWS_SECRET_ACCESS_KEY"] = v elif k == "AWS_DEFAULT_REGION": env["AWS_DEFAULT_REGION"] = v diff --git a/core/separation_of_duties.py b/core/separation_of_duties.py index e3b893a..fd35a28 100644 --- a/core/separation_of_duties.py +++ b/core/separation_of_duties.py @@ -74,7 +74,7 @@ def route_halt_artifact(contract_id: str, violation_reason: str, sns.publish( TopicArn=topic_arn, Message=json.dumps(halt_payload), - Subject="ACDL SoD halt", + Subject="Nova SoD halt", ) print(f"[halt-artifact] SNS published contract={contract_id} " f"topic={topic_arn}", flush=True) diff --git a/scripts/push_consumer_image.py b/scripts/push_consumer_image.py index 672346f..8ba41d8 100644 --- a/scripts/push_consumer_image.py +++ b/scripts/push_consumer_image.py @@ -65,8 +65,8 @@ def main(): creds = _load_env(ENV_FILE) # Dual-read: NOVA_* preferred, ACDL_* fallback (G-106, removed in P5). - access_key = creds.get("NOVA_AWS_ACCESS_KEY_ID") or creds.get("ACDL_AWS_ACCESS_KEY_ID") - secret_key = creds.get("NOVA_AWS_SECRET_ACCESS_KEY") or creds.get("ACDL_AWS_SECRET_ACCESS_KEY") + access_key = creds.get("NOVA_AWS_ACCESS_KEY_ID") + secret_key = creds.get("NOVA_AWS_SECRET_ACCESS_KEY") region = creds.get("AWS_DEFAULT_REGION", AWS_REGION) if not access_key or not secret_key: print("FAIL: NOVA_AWS_ACCESS_KEY_ID / NOVA_AWS_SECRET_ACCESS_KEY missing", diff --git a/scripts/rotate_spike_key.sh b/scripts/rotate_spike_key.sh index 28e40f5..c4e78d8 100755 --- a/scripts/rotate_spike_key.sh +++ b/scripts/rotate_spike_key.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash # scripts/rotate_spike_key.sh - rotate the nova-spike-runner IAM access key. # -# Uses the bootstrap root key (NOVA_BOOTSTRAP_AWS_*, ACDL_BOOTSTRAP_AWS_* +# Uses the bootstrap root key (NOVA_BOOTSTRAP_AWS_* # fallback) from the env to: # 1. List nova-spike-runner's access keys. # 2. Create a new key. @@ -24,8 +24,8 @@ ENV_FILE="$ROOT/.env.secrets" fail() { echo "FAIL: $*" >&2; exit 1; } # Dual-read bootstrap creds: NOVA_* preferred, ACDL_* fallback (removed in P5). -: "${NOVA_BOOTSTRAP_AWS_ACCESS_KEY_ID:-${ACDL_BOOTSTRAP_AWS_ACCESS_KEY_ID:?set NOVA_BOOTSTRAP_AWS_ACCESS_KEY_ID (or ACDL_BOOTSTRAP_AWS_ACCESS_KEY_ID) to the root key}}" -: "${NOVA_BOOTSTRAP_AWS_SECRET_ACCESS_KEY:-${ACDL_BOOTSTRAP_AWS_SECRET_ACCESS_KEY:?set NOVA_BOOTSTRAP_AWS_SECRET_ACCESS_KEY (or ACDL_BOOTSTRAP_AWS_SECRET_ACCESS_KEY) to the root key}}" +: "${NOVA_BOOTSTRAP_AWS_ACCESS_KEY_ID:?set NOVA_BOOTSTRAP_AWS_ACCESS_KEY_ID to the root key}" +: "${NOVA_BOOTSTRAP_AWS_SECRET_ACCESS_KEY:?set NOVA_BOOTSTRAP_AWS_SECRET_ACCESS_KEY to the root key}" REGION="${AWS_DEFAULT_REGION:-us-east-1}" USER_NAME="nova-spike-runner" @@ -43,8 +43,8 @@ user = "nova-spike-runner" env_file = os.path.join(os.getcwd(), ".env.secrets") # Dual-read bootstrap creds: NOVA_* preferred, ACDL_* fallback (G-106, removed in P5). -bootstrap_key = os.environ.get("NOVA_BOOTSTRAP_AWS_ACCESS_KEY_ID") or os.environ["ACDL_BOOTSTRAP_AWS_ACCESS_KEY_ID"] -bootstrap_secret = os.environ.get("NOVA_BOOTSTRAP_AWS_SECRET_ACCESS_KEY") or os.environ["ACDL_BOOTSTRAP_AWS_SECRET_ACCESS_KEY"] +bootstrap_key = os.environ["NOVA_BOOTSTRAP_AWS_ACCESS_KEY_ID"] +bootstrap_secret = os.environ["NOVA_BOOTSTRAP_AWS_SECRET_ACCESS_KEY"] session = boto3.Session( aws_access_key_id=bootstrap_key, @@ -84,7 +84,7 @@ print(f"rotated key written to {env_file} (chmod 600)", file=sys.stderr) # Optionally upload to Gitea secrets. # Dual-read token: NOVA_GITEA_TOKEN preferred, ACDL_GITEA_TOKEN fallback (G-106). -gitea_token = os.environ.get("NOVA_GITEA_TOKEN") or os.environ.get("ACDL_GITEA_TOKEN") +gitea_token = os.environ.get("NOVA_GITEA_TOKEN") if gitea_token: import urllib.request base = "https://git.cloudinit.dev/api/v1/repos/continuous-intelligence/acdl/actions/secrets" diff --git a/scripts/run_l2_lifecycle_destroy.sh b/scripts/run_l2_lifecycle_destroy.sh index d28d067..e9dfdcb 100755 --- a/scripts/run_l2_lifecycle_destroy.sh +++ b/scripts/run_l2_lifecycle_destroy.sh @@ -4,7 +4,7 @@ # Usage: run_l2_lifecycle_destroy.sh # # Wraps run_platform.sh for L2 composition modules in the modules-lifecycle -# pipeline. Sets NOVA_REMOTE_STATE_KEY (ACDL_REMOTE_STATE_KEY fallback until +# pipeline. Sets NOVA_REMOTE_STATE_KEY (NOVA-only since P5, REQ-164) for # P5) to point to the CI VPC state. # # NOTE: unlike the L1 scripts (run_lifecycle_destroy.sh), the L2 path does @@ -26,7 +26,7 @@ MODULE="$1" # Lifecycle mode: "plan" (default) skips destroy; "full" runs the real destroy. # Dual-read: NOVA_* preferred, ACDL_* fallback (removed in P5). -LIFECYCLE_MODE="${NOVA_LIFECYCLE_MODE:-${ACDL_LIFECYCLE_MODE:-plan}}" +LIFECYCLE_MODE="${NOVA_LIFECYCLE_MODE:-plan}" if [ "$LIFECYCLE_MODE" != "full" ]; then echo "lifecycle mode=$LIFECYCLE_MODE — nothing to destroy (plan-only run), exiting 0" @@ -38,7 +38,7 @@ CONTRACT="modules/l2/${MODULE}/examples/complex.yml" # Point terraform_remote_state to the CI VPC state (not the platform VPC). # Set both NOVA_* (preferred) and ACDL_* (legacy fallback) until P5. export NOVA_REMOTE_STATE_KEY="spike/ci-vpc/terraform.tfstate" -export ACDL_REMOTE_STATE_KEY="spike/ci-vpc/terraform.tfstate" # legacy fallback, removed in P5 + # Run the platform lifecycle destroy command bash scripts/run_platform.sh --destroy "$CONTRACT" \ No newline at end of file diff --git a/scripts/run_l2_lifecycle_test.sh b/scripts/run_l2_lifecycle_test.sh index 2d9a67f..48960b9 100755 --- a/scripts/run_l2_lifecycle_test.sh +++ b/scripts/run_l2_lifecycle_test.sh @@ -4,7 +4,7 @@ # Usage: run_l2_lifecycle_test.sh # # Wraps run_platform.sh for L2 composition modules in the modules-lifecycle -# pipeline. Sets NOVA_REMOTE_STATE_KEY (ACDL_REMOTE_STATE_KEY fallback until +# pipeline. Sets NOVA_REMOTE_STATE_KEY (NOVA-only since P5, REQ-164) for # P5) to point to the CI VPC state so the microservice composition's # terraform_remote_state data source reads from the short-lived CI VPC # (not the long-lived platform VPC). @@ -30,7 +30,7 @@ EXAMPLE="$2" # simple or complex # Lifecycle mode: "plan" (default, fast) or "full" (real apply against AWS). # Dual-read: NOVA_* preferred, ACDL_* fallback (removed in P5). -LIFECYCLE_MODE="${NOVA_LIFECYCLE_MODE:-${ACDL_LIFECYCLE_MODE:-plan}}" +LIFECYCLE_MODE="${NOVA_LIFECYCLE_MODE:-plan}" CONTRACT="modules/l2/${MODULE}/examples/${EXAMPLE}.yml" @@ -38,7 +38,7 @@ CONTRACT="modules/l2/${MODULE}/examples/${EXAMPLE}.yml" # Set both NOVA_* (preferred by the dual-read helper) and ACDL_* (legacy # fallback) so any unmigrated reader finds the key until P5. export NOVA_REMOTE_STATE_KEY="spike/ci-vpc/terraform.tfstate" -export ACDL_REMOTE_STATE_KEY="spike/ci-vpc/terraform.tfstate" # legacy fallback, removed in P5 + # Run the platform lifecycle command (plan-only by default; full = apply). if [ "$LIFECYCLE_MODE" = "full" ]; then diff --git a/scripts/run_lifecycle_destroy.sh b/scripts/run_lifecycle_destroy.sh index dd3b8e9..43c380e 100755 --- a/scripts/run_lifecycle_destroy.sh +++ b/scripts/run_lifecycle_destroy.sh @@ -21,7 +21,7 @@ CI_VPC_OUTPUTS="${2:-}" # Lifecycle mode: "plan" (default) skips destroy (nothing was applied); # "full" runs the real terraform destroy. # Dual-read: NOVA_* preferred, ACDL_* fallback (removed in P5). -LIFECYCLE_MODE="${NOVA_LIFECYCLE_MODE:-${ACDL_LIFECYCLE_MODE:-plan}}" +LIFECYCLE_MODE="${NOVA_LIFECYCLE_MODE:-plan}" if [ "$LIFECYCLE_MODE" != "full" ]; then echo "lifecycle mode=$LIFECYCLE_MODE — nothing to destroy (plan-only run), exiting 0" diff --git a/scripts/run_lifecycle_test.sh b/scripts/run_lifecycle_test.sh index d229e0e..2567112 100755 --- a/scripts/run_lifecycle_test.sh +++ b/scripts/run_lifecycle_test.sh @@ -27,7 +27,7 @@ CI_VPC_OUTPUTS="${3:-}" # Lifecycle mode: "plan" (default, fast) or "full" (real apply against AWS). # Dual-read: NOVA_* preferred, ACDL_* fallback (removed in P5). -LIFECYCLE_MODE="${NOVA_LIFECYCLE_MODE:-${ACDL_LIFECYCLE_MODE:-plan}}" +LIFECYCLE_MODE="${NOVA_LIFECYCLE_MODE:-plan}" CONTRACT="modules/l1/${MODULE}/examples/${EXAMPLE}.yml" diff --git a/scripts/run_platform.sh b/scripts/run_platform.sh index 1648c5a..966031a 100755 --- a/scripts/run_platform.sh +++ b/scripts/run_platform.sh @@ -121,9 +121,9 @@ if [ "$LOCAL_TIER" = "1" ]; then echo "=== Nova Local Emulating Tier (D-092) ===" echo "contract: $CONTRACT (no AWS credentials required)" echo "" - # Dual-read: set NOVA_LOCAL_TIER (preferred); ACDL_LOCAL_TIER fallback + # P5 (REQ-164): NOVA_* only (dual-read fallback removed) # kept for any unmigrated reader until P5 (removed in P5). - NOVA_LOCAL_TIER=1 ACDL_LOCAL_TIER=1 python3 core/local_emulators.py "$CONTRACT" \ + NOVA_LOCAL_TIER=1 python3 core/local_emulators.py "$CONTRACT" \ || fail "local E2E failed" echo "" echo "=== LOCAL E2E OK ===" @@ -287,14 +287,12 @@ if [ -z "${AWS_ACCESS_KEY_ID:-}" ] || [ -z "${AWS_SECRET_ACCESS_KEY:-}" ]; then [ -f "$ENV_FILE" ] || fail ".env.secrets missing (run scripts/rotate_spike_key.sh) or set AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY env vars" set -a . "$ENV_FILE" - set +a - # G-106 binding: dual-read NOVA_* first, ACDL_* fallback. The .env.secrets - # keys are renamed to NOVA_* in P2; the ACDL_* fallback covers operators - # who haven't rotated their local .env.secrets yet. Removed in P5. - export AWS_ACCESS_KEY_ID="${NOVA_AWS_ACCESS_KEY_ID:-$ACDL_AWS_ACCESS_KEY_ID}" - export AWS_SECRET_ACCESS_KEY="${NOVA_AWS_SECRET_ACCESS_KEY:-$ACDL_AWS_SECRET_ACCESS_KEY}" - export AWS_DEFAULT_REGION="$AWS_DEFAULT_REGION" -fi + set +a + # P5 (REQ-164): dual-read fallback removed — NOVA_* only. + export AWS_ACCESS_KEY_ID="$NOVA_AWS_ACCESS_KEY_ID" + export AWS_SECRET_ACCESS_KEY="$NOVA_AWS_SECRET_ACCESS_KEY" + export AWS_DEFAULT_REGION="$AWS_DEFAULT_REGION" + fi echo "=== Step 4: terraform init + validate + plan -lock=false (real AWS) ===" cd "$TF_DIR" @@ -349,7 +347,7 @@ if ok: else: print(f'HITL BLOCK: {reason}', file=sys.stderr) sys.exit(1) -" NOVA_HITL_CONTRACT_ID="$CONTRACT_ID" NOVA_HITL_ENV="$RESOLVED_ENV" NOVA_HITL_APPROVER="$APPROVER" ACDL_HITL_CONTRACT_ID="$CONTRACT_ID" ACDL_HITL_ENV="$RESOLVED_ENV" ACDL_HITL_APPROVER="$APPROVER" || { echo "FAIL: HITL attestation gate blocked the apply" >&2; exit 1; } +" NOVA_HITL_CONTRACT_ID="$CONTRACT_ID" NOVA_HITL_ENV="$RESOLVED_ENV" NOVA_HITL_APPROVER="$APPROVER" || { echo "FAIL: HITL attestation gate blocked the apply" >&2; exit 1; } else echo "Environment is dev — autonomous (no HITL gate)." fi @@ -465,7 +463,7 @@ if ok: else: print(f'HITL BLOCK: {reason}', file=sys.stderr) sys.exit(1) -" NOVA_HITL_CONTRACT_ID="$CONTRACT_ID" NOVA_HITL_ENV="$RESOLVED_ENV" NOVA_HITL_APPROVER="$APPROVER" ACDL_HITL_CONTRACT_ID="$CONTRACT_ID" ACDL_HITL_ENV="$RESOLVED_ENV" ACDL_HITL_APPROVER="$APPROVER" || { echo "FAIL: HITL attestation gate blocked the promotion" >&2; exit 1; } +" NOVA_HITL_CONTRACT_ID="$CONTRACT_ID" NOVA_HITL_ENV="$RESOLVED_ENV" NOVA_HITL_APPROVER="$APPROVER" || { echo "FAIL: HITL attestation gate blocked the promotion" >&2; exit 1; } else echo "Environment is dev — autonomous (no HITL gate)." fi diff --git a/scripts/run_regression.sh b/scripts/run_regression.sh index c15d00c..a13e34f 100755 --- a/scripts/run_regression.sh +++ b/scripts/run_regression.sh @@ -20,7 +20,7 @@ cd "$ROOT" echo "=== Nova Regression VERIFY (D-091) ===" # Dual-read: NOVA_* preferred, ACDL_* fallback (removed in P5). -echo "milestone: ${NOVA_REGRESSION_MILESTONE:-${ACDL_REGRESSION_MILESTONE:-v1.10}} phase: ${NOVA_REGRESSION_PHASE:-${ACDL_REGRESSION_PHASE:-52}}" +echo "milestone: ${NOVA_REGRESSION_MILESTONE:-v1.10} phase: ${NOVA_REGRESSION_PHASE:-52}" echo "" python3 core/regression_verify.py diff --git a/tests/test_env_helper.py b/tests/test_env_helper.py index 09ca516..4e82f03 100644 --- a/tests/test_env_helper.py +++ b/tests/test_env_helper.py @@ -1,14 +1,11 @@ -"""Unit tests for the dual-read env helper (core/env.py, D-108, REQ-159). +"""Unit tests for the NOVA-only env helper (core/env.py, D-108, REQ-164). -Covers the four cases: - - both NOVA_* and ACDL_* set (NOVA wins) - - only NOVA_* set - - only ACDL_* set (fallback) - - neither set (default returned) - -The ACDL_* fallback is the intentional dual-read source and is removed -in P5 (REQ-164). These fixtures deliberately keep the ACDL_* names as -the fallback source — they are the one allowed ACDL_* reference. +P5 (REQ-164) removed the ACDL_* dual-read fallback. `get_env` now reads +`NOVA_*` only. These tests verify: + - only NOVA_* set → returned + - neither set → default returned + - ACDL_* set but NOVA_* unset → default returned (fallback REMOVED) + - blank NOVA_* → default returned (not the ACDL_* value) """ from __future__ import annotations @@ -27,30 +24,33 @@ def _isolate_env(monkeypatch): yield -def test_both_set_nova_wins(monkeypatch): - monkeypatch.setenv("NOVA_AWS_ACCOUNT_ID", "nova-value") - monkeypatch.setenv("ACDL_AWS_ACCOUNT_ID", "acdl-value") - assert env.get_env("AWS_ACCOUNT_ID") == "nova-value" - - def test_only_nova_set(monkeypatch): monkeypatch.setenv("NOVA_AWS_ACCOUNT_ID", "nova-value") assert env.get_env("AWS_ACCOUNT_ID") == "nova-value" -def test_only_acdl_set_fallback(monkeypatch): - # ACDL_* is the intentional dual-read fallback source (removed in P5). - monkeypatch.setenv("ACDL_AWS_ACCOUNT_ID", "acdl-value") - assert env.get_env("AWS_ACCOUNT_ID") == "acdl-value" - - def test_neither_set_returns_default(): assert env.get_env("AWS_ACCOUNT_ID") is None assert env.get_env("AWS_ACCOUNT_ID", default="581513795199") == "581513795199" -def test_blank_nova_falls_back_to_acdl(monkeypatch): - # An explicitly-empty NOVA key must not shadow the ACDL fallback. +def test_only_acdl_set_no_fallback(monkeypatch): + # P5 (REQ-164): ACDL_* fallback removed — ACDL_* alone returns default. + monkeypatch.setenv("ACDL_AWS_ACCOUNT_ID", "acdl-value") + assert env.get_env("AWS_ACCOUNT_ID") is None + assert env.get_env("AWS_ACCOUNT_ID", default="fallback") == "fallback" + + +def test_both_set_nova_wins(monkeypatch): + # NOVA_* takes precedence; ACDL_* is ignored. + monkeypatch.setenv("NOVA_AWS_ACCOUNT_ID", "nova-value") + monkeypatch.setenv("ACDL_AWS_ACCOUNT_ID", "acdl-value") + assert env.get_env("AWS_ACCOUNT_ID") == "nova-value" + + +def test_blank_nova_returns_default_not_acdl(monkeypatch): + # An explicitly-empty NOVA key returns default (not an ACDL_* value). monkeypatch.setenv("NOVA_AWS_ACCOUNT_ID", "") monkeypatch.setenv("ACDL_AWS_ACCOUNT_ID", "acdl-value") - assert env.get_env("AWS_ACCOUNT_ID") == "acdl-value" \ No newline at end of file + assert env.get_env("AWS_ACCOUNT_ID") is None + assert env.get_env("AWS_ACCOUNT_ID", default="d") == "d" \ No newline at end of file diff --git a/tests/test_lifecycle_mode_flag.py b/tests/test_lifecycle_mode_flag.py index 0b526fd..7dbc35f 100644 --- a/tests/test_lifecycle_mode_flag.py +++ b/tests/test_lifecycle_mode_flag.py @@ -32,15 +32,15 @@ class TestLifecycleModeFlag: """REQ-134: the lifecycle scripts dispatch to plan-only by default.""" @pytest.mark.parametrize("script", SCRIPTS) - def test_script_reads_acdl_lifecycle_mode(self, script): - """Every lifecycle script reads NOVA_LIFECYCLE_MODE (dual-read with - ACDL_LIFECYCLE_MODE fallback) with a 'plan' default.""" + def test_script_reads_nova_lifecycle_mode(self, script): + """Every lifecycle script reads NOVA_LIFECYCLE_MODE (NOVA-only since + P5, REQ-164 — dual-read fallback removed) with a 'plan' default.""" src = _read(script) - # P2 (REQ-159): dual-read NOVA_* preferred, ACDL_* fallback. The - # ACDL_LIFECYCLE_MODE:-plan substring is still present inside the - # nested dual-read expression (removed in P5). - assert "${NOVA_LIFECYCLE_MODE:-${ACDL_LIFECYCLE_MODE:-plan}}" in src, \ - f"{script} must dual-read NOVA_/ACDL_LIFECYCLE_MODE defaulting to 'plan'" + # P5 (REQ-164): NOVA_* only (dual-read fallback removed). + assert "${NOVA_LIFECYCLE_MODE:-plan}" in src, \ + f"{script} must read NOVA_LIFECYCLE_MODE defaulting to 'plan'" + assert "ACDL_LIFECYCLE_MODE" not in src, \ + f"{script} must not reference ACDL_LIFECYCLE_MODE (P5 removed the fallback)" assert "LIFECYCLE_MODE=" in src, \ f"{script} must assign LIFECYCLE_MODE from the env var" diff --git a/tests/test_output_publisher.py b/tests/test_output_publisher.py index 85c1ac5..ea6b802 100644 --- a/tests/test_output_publisher.py +++ b/tests/test_output_publisher.py @@ -43,7 +43,7 @@ class TestPublishToSsm: monkeypatch.setenv("AWS_DEFAULT_REGION", "us-east-1") monkeypatch.setenv("AWS_ACCESS_KEY_ID", "testing") monkeypatch.setenv("AWS_SECRET_ACCESS_KEY", "testing") - monkeypatch.setenv("ACDL_KMS_KEY_ID", "alias/aws/ssm") + monkeypatch.setenv("NOVA_KMS_KEY_ID", "alias/aws/ssm") with mock_aws(): ssm = boto3.client("ssm", region_name="us-east-1") @@ -67,7 +67,7 @@ class TestPublishToSsm: monkeypatch.setenv("AWS_DEFAULT_REGION", "us-east-1") monkeypatch.setenv("AWS_ACCESS_KEY_ID", "testing") monkeypatch.setenv("AWS_SECRET_ACCESS_KEY", "testing") - monkeypatch.setenv("ACDL_KMS_KEY_ID", "alias/aws/ssm") + monkeypatch.setenv("NOVA_KMS_KEY_ID", "alias/aws/ssm") with mock_aws(): ssm = boto3.client("ssm", region_name="us-east-1") @@ -82,7 +82,7 @@ class TestPublishToSsm: monkeypatch.setenv("AWS_DEFAULT_REGION", "us-east-1") monkeypatch.setenv("AWS_ACCESS_KEY_ID", "testing") monkeypatch.setenv("AWS_SECRET_ACCESS_KEY", "testing") - monkeypatch.setenv("ACDL_KMS_KEY_ID", "alias/aws/ssm") + monkeypatch.setenv("NOVA_KMS_KEY_ID", "alias/aws/ssm") with mock_aws(): ssm = boto3.client("ssm", region_name="us-east-1") @@ -105,7 +105,7 @@ class TestPublishToSsm: monkeypatch.setenv("AWS_DEFAULT_REGION", "us-east-1") monkeypatch.setenv("AWS_ACCESS_KEY_ID", "testing") monkeypatch.setenv("AWS_SECRET_ACCESS_KEY", "testing") - monkeypatch.setenv("ACDL_KMS_KEY_ID", "alias/aws/ssm") + monkeypatch.setenv("NOVA_KMS_KEY_ID", "alias/aws/ssm") with mock_aws(): ssm = boto3.client("ssm", region_name="us-east-1") @@ -123,7 +123,7 @@ class TestPublishToSsm: monkeypatch.setenv("AWS_DEFAULT_REGION", "us-east-1") monkeypatch.setenv("AWS_ACCESS_KEY_ID", "testing") monkeypatch.setenv("AWS_SECRET_ACCESS_KEY", "testing") - monkeypatch.setenv("ACDL_KMS_KEY_ID", "alias/aws/ssm") + monkeypatch.setenv("NOVA_KMS_KEY_ID", "alias/aws/ssm") with mock_aws(): ssm = boto3.client("ssm", region_name="us-east-1") @@ -377,7 +377,7 @@ class TestCli: class TestKmsFailLoud: """P1-3: SSM publisher must fail loud when NOVA_KMS_KEY_ID is unset - (P2 renamed from ACDL_KMS_KEY_ID; dual-read NOVA_* preferred, + (P2 renamed from NOVA_KMS_KEY_ID; dual-read NOVA_* preferred, ACDL_* fallback until P5).""" def test_kms_unset_raises(self, monkeypatch): @@ -389,9 +389,9 @@ class TestKmsFailLoud: monkeypatch.setenv("AWS_SECRET_ACCESS_KEY", "testing") # Both NOVA_* and ACDL_* unset → helper returns default (None) → fail loud. monkeypatch.delenv("NOVA_KMS_KEY_ID", raising=False) - monkeypatch.delenv("ACDL_KMS_KEY_ID", raising=False) + monkeypatch.delenv("NOVA_KMS_KEY_ID", raising=False) + monkeypatch.delenv("NOVA_ALLOW_DEFAULT_KMS", raising=False) monkeypatch.delenv("NOVA_ALLOW_DEFAULT_KMS", raising=False) - monkeypatch.delenv("ACDL_ALLOW_DEFAULT_KMS", raising=False) with mock_aws(): with pytest.raises(RuntimeError, match="NOVA_KMS_KEY_ID is not set"): @@ -405,7 +405,7 @@ class TestKmsFailLoud: monkeypatch.setenv("AWS_ACCESS_KEY_ID", "testing") monkeypatch.setenv("AWS_SECRET_ACCESS_KEY", "testing") monkeypatch.delenv("NOVA_KMS_KEY_ID", raising=False) - monkeypatch.delenv("ACDL_KMS_KEY_ID", raising=False) + monkeypatch.delenv("NOVA_KMS_KEY_ID", raising=False) monkeypatch.setenv("NOVA_ALLOW_DEFAULT_KMS", "1") with mock_aws(): diff --git a/tests/test_route_halt_artifact.py b/tests/test_route_halt_artifact.py index cd32442..16405a1 100644 --- a/tests/test_route_halt_artifact.py +++ b/tests/test_route_halt_artifact.py @@ -24,7 +24,7 @@ def test_route_halt_publishes_to_sns_when_arn_set(monkeypatch): assert call.kwargs["TopicArn"] == "arn:aws:sns:us-east-1:000000000000:nova-sod-halt" assert "contract-123" in call.kwargs["Message"] assert "SEPARATION_OF_DUTIES_VIOLATION" in call.kwargs["Message"] - assert call.kwargs["Subject"] == "ACDL SoD halt" + assert call.kwargs["Subject"] == "Nova SoD halt" def test_route_halt_falls_back_to_stderr_when_arn_unset(monkeypatch, capsys):