@@ -1027,19 +1027,98 @@ def repository_state(
10271027) -> dict [str , Any ]:
10281028 graph = dict (graph ) if graph is not None else load_graph ()
10291029 _manifest , source_tree = git_manifest (repository , revision )
1030- environment = environment_fingerprint ()
1030+ environment = validate_environment_fingerprint (environment_fingerprint ())
1031+ component_environment = {
1032+ component : validate_environment_fingerprint (component_environment_fingerprint (component ))
1033+ for component in PLAN_COMPONENTS
1034+ }
10311035 payload = {
1036+ "component_environment" : component_environment ,
10321037 "component_environment_sha256" : {
1033- component : component_environment_fingerprint ( component ) ["sha256" ]
1034- for component in PLAN_COMPONENTS
1038+ component : fingerprint ["sha256" ]
1039+ for component , fingerprint in component_environment . items ()
10351040 },
1041+ "environment" : environment ,
10361042 "environment_sha256" : environment ["sha256" ],
10371043 "graph_sha256" : graph_digest (graph ),
10381044 "policy_version" : graph ["policy_version" ],
10391045 "source_manifest_sha256" : source_tree ["manifest_sha256" ],
10401046 "tree_oid" : source_tree ["tree_oid" ],
10411047 }
1042- return payload | {"verification_state_sha256" : sha256_json (payload )}
1048+ return _validate_repository_state (payload | {"verification_state_sha256" : sha256_json (payload )})
1049+
1050+
1051+ def _validate_repository_state (value : object ) -> dict [str , Any ]:
1052+ """Validate the aggregate state retained for scheduled coverage decisions."""
1053+ expected = {
1054+ "component_environment" ,
1055+ "component_environment_sha256" ,
1056+ "environment" ,
1057+ "environment_sha256" ,
1058+ "graph_sha256" ,
1059+ "policy_version" ,
1060+ "source_manifest_sha256" ,
1061+ "tree_oid" ,
1062+ "verification_state_sha256" ,
1063+ }
1064+ if not isinstance (value , dict ) or set (value ) != expected :
1065+ raise VerificationError ("repository state has an invalid shape" )
1066+ try :
1067+ environment = validate_environment_fingerprint (value ["environment" ])
1068+ component_environment = value ["component_environment" ]
1069+ if not isinstance (component_environment , dict ) or set (component_environment ) != set (
1070+ PLAN_COMPONENTS
1071+ ):
1072+ raise VerificationError ("repository state component environments are incomplete" )
1073+ validated_components = {
1074+ component : validate_environment_fingerprint (component_environment [component ])
1075+ for component in PLAN_COMPONENTS
1076+ }
1077+ except (TypeError , ValueError ) as exc :
1078+ raise VerificationError ("repository state environment fingerprint is invalid" ) from exc
1079+
1080+ if value ["environment_sha256" ] != environment ["sha256" ]:
1081+ raise VerificationError ("repository state global environment digest does not match" )
1082+ component_digests = value ["component_environment_sha256" ]
1083+ if (
1084+ not isinstance (component_digests , dict )
1085+ or set (component_digests ) != set (PLAN_COMPONENTS )
1086+ or component_digests
1087+ != {
1088+ component : fingerprint ["sha256" ]
1089+ for component , fingerprint in validated_components .items ()
1090+ }
1091+ ):
1092+ raise VerificationError ("repository state component environment digests do not match" )
1093+ for field in ("graph_sha256" , "source_manifest_sha256" , "verification_state_sha256" ):
1094+ if not isinstance (value [field ], str ) or not SHA256_RE .fullmatch (value [field ]):
1095+ raise VerificationError ("repository state digest is invalid" )
1096+ if not isinstance (value ["tree_oid" ], str ) or not SHA_RE .fullmatch (value ["tree_oid" ]):
1097+ raise VerificationError ("repository state tree identity is invalid" )
1098+ if not isinstance (value ["policy_version" ], int ) or isinstance (value ["policy_version" ], bool ):
1099+ raise VerificationError ("repository state policy version is invalid" )
1100+
1101+ identity = dict (value )
1102+ digest = identity .pop ("verification_state_sha256" )
1103+ if digest != sha256_json (identity ):
1104+ raise VerificationError ("repository state digest does not match its contents" )
1105+ return value
1106+
1107+
1108+ def _scheduled_environment_matches (
1109+ actual : Mapping [str , Any ],
1110+ planned : Mapping [str , Any ],
1111+ * ,
1112+ allow_hosted_runner_drift : bool ,
1113+ ) -> bool :
1114+ """Apply exact state identity by default; opt into hosted-runner drift explicitly."""
1115+ if not allow_hosted_runner_drift :
1116+ return actual == planned
1117+ return environment_matches_plan (
1118+ actual ,
1119+ planned ,
1120+ allow_hosted_runner_drift = True ,
1121+ )
10431122
10441123
10451124def build_scheduled_state_envelope (
@@ -1050,22 +1129,35 @@ def build_scheduled_state_envelope(
10501129 run_id : int ,
10511130 run_attempt : int ,
10521131 workflow : str = ".github/workflows/scheduled-full-regression.yml" ,
1132+ allow_hosted_runner_drift : bool = False ,
10531133) -> dict [str , Any ]:
10541134 plan = validate_plan (dict (plan ))
10551135 report = validate_report (dict (report ))
1136+ state = _validate_repository_state (dict (state ))
10561137 if report ["verdict" ] != "success" or report ["plan_sha256" ] != sha256_json (plan ):
10571138 raise VerificationError ("scheduled state requires a successful plan-bound report" )
1139+ planned_component_environment = {
1140+ component : item ["environment" ] for component , item in plan ["components" ].items ()
1141+ }
10581142 if (
1059- state .get ("verification_state_sha256" ) is None
1060- or state .get ("source_manifest_sha256" ) != plan ["source_tree" ]["manifest_sha256" ]
1061- or state .get ("graph_sha256" ) != plan ["graph_sha256" ]
1062- or state .get ("policy_version" ) != plan ["policy_version" ]
1063- or state .get ("environment_sha256" ) != plan ["environment" ]["sha256" ]
1064- or state .get ("component_environment_sha256" )
1065- != {
1066- component : item ["environment" ]["sha256" ]
1067- for component , item in plan ["components" ].items ()
1068- }
1143+ state ["source_manifest_sha256" ] != plan ["source_tree" ]["manifest_sha256" ]
1144+ or state ["tree_oid" ] != plan ["source_tree" ]["tree_oid" ]
1145+ or state ["graph_sha256" ] != plan ["graph_sha256" ]
1146+ or state ["policy_version" ] != plan ["policy_version" ]
1147+ or not _scheduled_environment_matches (
1148+ state ["environment" ],
1149+ plan ["environment" ],
1150+ allow_hosted_runner_drift = allow_hosted_runner_drift ,
1151+ )
1152+ or set (state ["component_environment" ]) != set (planned_component_environment )
1153+ or any (
1154+ not _scheduled_environment_matches (
1155+ state ["component_environment" ][component ],
1156+ planned_environment ,
1157+ allow_hosted_runner_drift = allow_hosted_runner_drift ,
1158+ )
1159+ for component , planned_environment in planned_component_environment .items ()
1160+ )
10691161 ):
10701162 raise VerificationError ("scheduled repository state does not match the plan" )
10711163 evidence_ids = sorted (
@@ -1690,6 +1782,7 @@ def main() -> None:
16901782 scheduled_state_parser .add_argument ("--state" , required = True )
16911783 scheduled_state_parser .add_argument ("--run-id" , type = int , required = True )
16921784 scheduled_state_parser .add_argument ("--run-attempt" , type = int , required = True )
1785+ scheduled_state_parser .add_argument ("--allow-hosted-runner-drift" , action = "store_true" )
16931786 scheduled_state_parser .add_argument ("--output" , required = True )
16941787
16951788 args = parser .parse_args ()
@@ -1841,6 +1934,7 @@ def main() -> None:
18411934 state = state ,
18421935 run_id = args .run_id ,
18431936 run_attempt = args .run_attempt ,
1937+ allow_hosted_runner_drift = args .allow_hosted_runner_drift ,
18441938 ),
18451939 args .output ,
18461940 )
0 commit comments