Skip to content

Commit 12bc523

Browse files
Pigbibicodex
andcommitted
feat: validate strategy release target identity
Co-Authored-By: Codex <noreply@openai.com>
1 parent fae26ea commit 12bc523

3 files changed

Lines changed: 141 additions & 0 deletions

File tree

python/scripts/runtime_settings.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import subprocess
1212
import sys
1313
from dataclasses import dataclass
14+
from datetime import date
1415
from pathlib import Path
1516
from typing import Any
1617
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
@@ -53,6 +54,27 @@
5354
}
5455
SCHEDULER_FIELDS = frozenset({"timezone", "main_time", "probe_time", "precheck_time"})
5556
MARKET_FIELDS = ("market", "market_calendar", "market_timezone")
57+
STRATEGY_RELEASE_REQUIRED_FIELDS = (
58+
"release_id",
59+
"manifest_sha256",
60+
"strategy_revision",
61+
"config_sha256",
62+
"risk_policy_sha256",
63+
"evidence_sha256",
64+
"plugin_bundle_sha256",
65+
"effective_session",
66+
)
67+
STRATEGY_RELEASE_DIGEST_FIELDS = frozenset(
68+
{
69+
"manifest_sha256",
70+
"config_sha256",
71+
"risk_policy_sha256",
72+
"evidence_sha256",
73+
"plugin_bundle_sha256",
74+
}
75+
)
76+
STRATEGY_RELEASE_ID_PATTERN = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{2,127}$")
77+
SHA256_PATTERN = re.compile(r"^(?:sha256:)?[0-9a-fA-F]{64}$")
5678
GENERATED_VARIABLES = {"RUNTIME_TARGET_JSON", "STRATEGY_PROFILE"}
5779
SECRET_MARKERS = ("PASSWORD", "PRIVATE_KEY", "TOKEN", "API_KEY", "ACCESS_KEY", "CLIENT_SECRET", "SECRET")
5880
LEGACY_INCOME_LAYER_VARIABLES = frozenset(
@@ -425,6 +447,45 @@ def validate_runtime_target(target: dict[str, Any], errors: list[str]) -> None:
425447
ZoneInfo(market_timezone)
426448
except (ZoneInfoNotFoundError, ValueError):
427449
errors.append(f"runtime_target.market_timezone is invalid: {market_timezone!r}")
450+
validate_strategy_release(runtime_target, errors)
451+
452+
453+
def validate_strategy_release(runtime_target: dict[str, Any], errors: list[str]) -> None:
454+
"""Validate an optional immutable release identity without enabling it.
455+
456+
Existing targets intentionally remain valid without ``strategy_release``
457+
during the read-only migration. Once present, however, a partial identity
458+
is never accepted because it could be mistaken for a verified release.
459+
"""
460+
461+
release = runtime_target.get("strategy_release")
462+
if release is None:
463+
return
464+
if not isinstance(release, dict):
465+
errors.append("runtime_target.strategy_release must be an object when present")
466+
return
467+
unexpected = sorted(set(release) - set(STRATEGY_RELEASE_REQUIRED_FIELDS))
468+
if unexpected:
469+
errors.append(
470+
"runtime_target.strategy_release contains unsupported fields: "
471+
+ ", ".join(unexpected)
472+
)
473+
for field in STRATEGY_RELEASE_REQUIRED_FIELDS:
474+
value = release.get(field)
475+
if not isinstance(value, str) or not value.strip():
476+
errors.append(f"runtime_target.strategy_release.{field} is required")
477+
continue
478+
if field == "release_id" and not STRATEGY_RELEASE_ID_PATTERN.fullmatch(value.strip()):
479+
errors.append("runtime_target.strategy_release.release_id has invalid characters")
480+
if field in STRATEGY_RELEASE_DIGEST_FIELDS and not SHA256_PATTERN.fullmatch(value.strip()):
481+
errors.append(f"runtime_target.strategy_release.{field} must be a SHA-256 digest")
482+
if field == "effective_session":
483+
try:
484+
date.fromisoformat(value.strip())
485+
except ValueError:
486+
errors.append(
487+
"runtime_target.strategy_release.effective_session must be an ISO-8601 date"
488+
)
428489

429490

430491
def validate_live_ibkr_us_scheduler(

python/tests/test_runtime_settings.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -615,6 +615,38 @@ def test_runtime_target_json_is_canonical_source_for_strategy_profile(self):
615615
self.assertEqual(assignments["STRATEGY_PROFILE"], target["runtime_target"]["strategy_profile"])
616616
self.assertNotIn("STRATEGY_PROFILE", target["extra_variables"])
617617

618+
def test_runtime_target_accepts_complete_optional_strategy_release_identity(self):
619+
_, target = self.load_target("examples/targets/schwab/live.example.json")
620+
digest = "a" * 64
621+
target["runtime_target"]["strategy_release"] = {
622+
"release_id": "soxl-p2-v3.20260824",
623+
"manifest_sha256": digest,
624+
"strategy_revision": "2e3bb51",
625+
"config_sha256": digest,
626+
"risk_policy_sha256": digest,
627+
"evidence_sha256": digest,
628+
"plugin_bundle_sha256": digest,
629+
"effective_session": "2026-08-25",
630+
}
631+
632+
self.assertEqual(runtime_settings.validate_target(target), [])
633+
634+
def test_runtime_target_rejects_partial_or_invalid_strategy_release_identity(self):
635+
_, target = self.load_target("examples/targets/schwab/live.example.json")
636+
target["runtime_target"]["strategy_release"] = {
637+
"release_id": "not valid",
638+
"manifest_sha256": "not-a-digest",
639+
}
640+
641+
errors = runtime_settings.validate_target(target)
642+
643+
self.assertIn("runtime_target.strategy_release.release_id has invalid characters", errors)
644+
self.assertIn(
645+
"runtime_target.strategy_release.manifest_sha256 must be a SHA-256 digest",
646+
errors,
647+
)
648+
self.assertIn("runtime_target.strategy_release.strategy_revision is required", errors)
649+
618650
def test_example_targets_have_matching_plugin_mount(self):
619651
for relative_path in (
620652
"examples/targets/schwab/live.example.json",

schemas/runtime-target.schema.json

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,54 @@
151151
}
152152
}
153153
}
154+
},
155+
"strategy_release": {
156+
"type": "object",
157+
"additionalProperties": false,
158+
"required": [
159+
"release_id",
160+
"manifest_sha256",
161+
"strategy_revision",
162+
"config_sha256",
163+
"risk_policy_sha256",
164+
"evidence_sha256",
165+
"plugin_bundle_sha256",
166+
"effective_session"
167+
],
168+
"properties": {
169+
"release_id": {
170+
"type": "string",
171+
"pattern": "^[A-Za-z0-9][A-Za-z0-9._-]{2,127}$"
172+
},
173+
"manifest_sha256": {
174+
"type": "string",
175+
"pattern": "^(sha256:)?[0-9a-fA-F]{64}$"
176+
},
177+
"strategy_revision": {
178+
"type": "string",
179+
"minLength": 1
180+
},
181+
"config_sha256": {
182+
"type": "string",
183+
"pattern": "^(sha256:)?[0-9a-fA-F]{64}$"
184+
},
185+
"risk_policy_sha256": {
186+
"type": "string",
187+
"pattern": "^(sha256:)?[0-9a-fA-F]{64}$"
188+
},
189+
"evidence_sha256": {
190+
"type": "string",
191+
"pattern": "^(sha256:)?[0-9a-fA-F]{64}$"
192+
},
193+
"plugin_bundle_sha256": {
194+
"type": "string",
195+
"pattern": "^(sha256:)?[0-9a-fA-F]{64}$"
196+
},
197+
"effective_session": {
198+
"type": "string",
199+
"format": "date"
200+
}
201+
}
154202
}
155203
}
156204
},

0 commit comments

Comments
 (0)