|
4 | 4 | from __future__ import annotations |
5 | 5 |
|
6 | 6 | import argparse |
| 7 | +import hashlib |
7 | 8 | import json |
8 | 9 | import os |
9 | 10 | import re |
|
75 | 76 | ) |
76 | 77 | STRATEGY_RELEASE_ID_PATTERN = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{2,127}$") |
77 | 78 | SHA256_PATTERN = re.compile(r"^(?:sha256:)?[0-9a-fA-F]{64}$") |
| 79 | +LIVE_CONTINUITY_STATES = frozenset( |
| 80 | + { |
| 81 | + "ACTIVE_LKG", |
| 82 | + "ACTIVE_REDUCED", |
| 83 | + "RECONCILE_ONLY", |
| 84 | + "RISK_REDUCTION_ONLY", |
| 85 | + "PAUSED", |
| 86 | + "ROLLBACK_LKG", |
| 87 | + } |
| 88 | +) |
| 89 | +LIVE_CONTINUITY_BASELINE_KINDS = frozenset({"legacy_authorized", "release_attested"}) |
| 90 | +LIVE_CONTINUITY_FIELDS = frozenset( |
| 91 | + { |
| 92 | + "state", |
| 93 | + "baseline_kind", |
| 94 | + "baseline_id", |
| 95 | + "baseline_target_sha256", |
| 96 | + "captured_at", |
| 97 | + } |
| 98 | +) |
| 99 | +LIVE_CONTINUITY_BASELINE_ID_PATTERN = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{2,127}$") |
78 | 100 | GENERATED_VARIABLES = {"RUNTIME_TARGET_JSON", "STRATEGY_PROFILE"} |
79 | 101 | SECRET_MARKERS = ("PASSWORD", "PRIVATE_KEY", "TOKEN", "API_KEY", "ACCESS_KEY", "CLIENT_SECRET", "SECRET") |
80 | 102 | LEGACY_INCOME_LAYER_VARIABLES = frozenset( |
@@ -187,6 +209,15 @@ def compact_json(value: Any) -> str: |
187 | 209 | return json.dumps(value, ensure_ascii=False, separators=(",", ":")) |
188 | 210 |
|
189 | 211 |
|
| 212 | +def runtime_target_fingerprint(runtime_target: dict[str, Any]) -> str: |
| 213 | + """Fingerprint a frozen target excluding only its current continuity state.""" |
| 214 | + |
| 215 | + payload = dict(runtime_target) |
| 216 | + payload.pop("live_continuity", None) |
| 217 | + encoded = json.dumps(payload, ensure_ascii=True, sort_keys=True, separators=(",", ":")) |
| 218 | + return hashlib.sha256(encoded.encode("utf-8")).hexdigest() |
| 219 | + |
| 220 | + |
190 | 221 | def env_string(value: Any) -> str: |
191 | 222 | if isinstance(value, str): |
192 | 223 | return value |
@@ -453,6 +484,7 @@ def validate_runtime_target(target: dict[str, Any], errors: list[str]) -> None: |
453 | 484 | except (ZoneInfoNotFoundError, ValueError): |
454 | 485 | errors.append(f"runtime_target.market_timezone is invalid: {market_timezone!r}") |
455 | 486 | validate_strategy_release(runtime_target, errors) |
| 487 | + validate_live_continuity(runtime_target, errors) |
456 | 488 |
|
457 | 489 |
|
458 | 490 | def validate_strategy_release(runtime_target: dict[str, Any], errors: list[str]) -> None: |
@@ -493,6 +525,62 @@ def validate_strategy_release(runtime_target: dict[str, Any], errors: list[str]) |
493 | 525 | ) |
494 | 526 |
|
495 | 527 |
|
| 528 | +def validate_live_continuity(runtime_target: dict[str, Any], errors: list[str]) -> None: |
| 529 | + """Validate a frozen incumbent baseline independently of candidate policy.""" |
| 530 | + |
| 531 | + continuity = runtime_target.get("live_continuity") |
| 532 | + if continuity is None: |
| 533 | + return |
| 534 | + if not isinstance(continuity, dict): |
| 535 | + errors.append("runtime_target.live_continuity must be an object when present") |
| 536 | + return |
| 537 | + unsupported = sorted(set(continuity) - LIVE_CONTINUITY_FIELDS) |
| 538 | + if unsupported: |
| 539 | + errors.append( |
| 540 | + "runtime_target.live_continuity contains unsupported fields: " + ", ".join(unsupported) |
| 541 | + ) |
| 542 | + for field in sorted(LIVE_CONTINUITY_FIELDS): |
| 543 | + value = continuity.get(field) |
| 544 | + if not isinstance(value, str) or not value.strip(): |
| 545 | + errors.append(f"runtime_target.live_continuity.{field} is required") |
| 546 | + |
| 547 | + state = str(continuity.get("state") or "").strip().upper() |
| 548 | + if state not in LIVE_CONTINUITY_STATES: |
| 549 | + errors.append( |
| 550 | + "runtime_target.live_continuity.state must be one of " |
| 551 | + + ", ".join(sorted(LIVE_CONTINUITY_STATES)) |
| 552 | + ) |
| 553 | + baseline_kind = str(continuity.get("baseline_kind") or "").strip() |
| 554 | + if baseline_kind not in LIVE_CONTINUITY_BASELINE_KINDS: |
| 555 | + errors.append( |
| 556 | + "runtime_target.live_continuity.baseline_kind must be one of " |
| 557 | + + ", ".join(sorted(LIVE_CONTINUITY_BASELINE_KINDS)) |
| 558 | + ) |
| 559 | + baseline_id = str(continuity.get("baseline_id") or "").strip() |
| 560 | + if baseline_id and not LIVE_CONTINUITY_BASELINE_ID_PATTERN.fullmatch(baseline_id): |
| 561 | + errors.append("runtime_target.live_continuity.baseline_id has invalid characters") |
| 562 | + digest = str(continuity.get("baseline_target_sha256") or "").strip().lower() |
| 563 | + digest = digest.removeprefix("sha256:") |
| 564 | + if not SHA256_PATTERN.fullmatch(digest): |
| 565 | + errors.append("runtime_target.live_continuity.baseline_target_sha256 must be a SHA-256 digest") |
| 566 | + elif digest != runtime_target_fingerprint(runtime_target): |
| 567 | + errors.append( |
| 568 | + "runtime_target.live_continuity.baseline_target_sha256 does not match the runtime target" |
| 569 | + ) |
| 570 | + captured_at = str(continuity.get("captured_at") or "").strip() |
| 571 | + if captured_at: |
| 572 | + try: |
| 573 | + date.fromisoformat(captured_at) |
| 574 | + except ValueError: |
| 575 | + errors.append("runtime_target.live_continuity.captured_at must be an ISO-8601 date") |
| 576 | + if baseline_kind == "release_attested" and not isinstance(runtime_target.get("strategy_release"), dict): |
| 577 | + errors.append("release_attested live_continuity requires runtime_target.strategy_release") |
| 578 | + |
| 579 | + |
| 580 | +def is_live_continuity_target(runtime_target: dict[str, Any]) -> bool: |
| 581 | + return isinstance(runtime_target.get("live_continuity"), dict) |
| 582 | + |
| 583 | + |
496 | 584 | def validate_live_ibkr_us_scheduler( |
497 | 585 | runtime_target: dict[str, Any], |
498 | 586 | scheduler: dict[str, Any], |
@@ -596,12 +684,26 @@ def validate_runtime_target_strategy_policy(runtime_target: dict[str, Any], erro |
596 | 684 | errors.append( |
597 | 685 | f"platform {platform_id} does not support {execution_mode} control execution" |
598 | 686 | ) |
| 687 | + continuity_target = is_live_continuity_target(runtime_target) |
599 | 688 | allowed_modes = normalize_allowed_execution_modes(strategy.get("allowed_execution_modes")) |
600 | | - if allowed_modes and execution_mode not in allowed_modes: |
| 689 | + if allowed_modes and execution_mode not in allowed_modes and not continuity_target: |
601 | 690 | errors.append(f"runtime_target.strategy_profile {profile} does not allow {execution_mode} execution") |
602 | 691 |
|
603 | 692 | if execution_mode != "live": |
604 | 693 | return |
| 694 | + if continuity_target: |
| 695 | + continuity_policy = strategy.get("live_continuity") |
| 696 | + if not isinstance(continuity_policy, dict) or continuity_policy.get("eligible") is not True: |
| 697 | + errors.append( |
| 698 | + f"runtime_target.strategy_profile {profile} is not eligible for live continuity" |
| 699 | + ) |
| 700 | + return |
| 701 | + allowed_platforms = continuity_policy.get("allowed_platforms") |
| 702 | + if not isinstance(allowed_platforms, list) or platform_id not in allowed_platforms: |
| 703 | + errors.append( |
| 704 | + f"runtime_target.strategy_profile {profile} live continuity is not allowed on {platform_id}" |
| 705 | + ) |
| 706 | + return |
605 | 707 | lifecycle_stage = str(strategy.get("lifecycle_stage") or "").strip() |
606 | 708 | if strategy.get("runtime_enabled") is not True: |
607 | 709 | errors.append(f"runtime_target.strategy_profile {profile} is not runtime_enabled") |
|
0 commit comments