Repository navigation
fix(runtime): compare mixed-runtime target manifests in canonical contract form #1482
Description
Gap Claim
MixedRuntimeBinding refuses a component whose runtime target carries the admitted backend manifest when the backend built that manifest in another internal representation.
_require_bound_component (implementations/python/packages/raes_runtime/mixed_runtime.py:194-214) runs for every admitted component when a binding is constructed (:88, :184-191). It first compares the configured backend-manifest-v2 model with the admitted profile: identity, version, canonical digest and envelope identity (:197-211). It then rebuilds an internal BackendManifest from that model with backend_manifest_from_v2_model_with_envelope and requires the target's manifest to equal the rebuild under dataclass == (:212-214). The rebuild does not reproduce how a backend holds its own manifest:
provisioner_from_model(implementations/python/packages/raes_backend_protocols/provisioner_manifest.py:66-73) restoresoperating_systemsas a tuple in the payload's row order.backend_manifest_v2_modelwrites those rows sorted by family and distribution (:20-30)._realization_support_from_model(implementations/python/packages/raes_backend_protocols/manifest.py:273-290) restoresartifact_mechanismsas a tuple (:288).- Neither
ProvisionerCapabilities(implementations/python/packages/raes_backend_protocols/provisioner_capabilities.py:152,__post_init__at:168-222) norRealizationSupportDeclaration(implementations/python/packages/raes_contracts/apparatus.py:151,__post_init__at:154-173) normalizes these sequences. A backend's manifest keeps the order and the container type it was built with. A list never equals a tuple, and two tuples with the same rows in another order are not equal.
LilRAE's APTL backend builds its manifest that way. At LilRAE dev (a5833df9), create_aptl_runtime_target puts create_aptl_manifest() on its RuntimeTarget (src/aptl/backends/raes.py:147-152). The provisioner keeps its operating-system rows in authored order: debian, ubuntu, rocky-linux, then the x-aptl: distributions (src/aptl/backends/raes_operating_systems.py:15-38, used at src/aptl/backends/raes_manifest.py:231). The realization support passes artifact_mechanisms=list(aptl_artifact_mechanisms()) (src/aptl/backends/raes_manifest.py:387).
Reproduction on rae dev (35122105) with the LilRAE dev source (a5833df9) on the path, using the rae project environment's Python 3.14.4 on 2026-10-10 at 19:52Z. The script below builds the manifest and the realization envelope with LilRAE's own functions. It admits the manifest's published payload, backend_manifest_payload(create_aptl_manifest()), which LilRAE records for provenance (src/aptl/core/provenance/providers/apparatus.py:39). Then it runs the check above on a target that carries LilRAE's manifest:
backend identity: aptl 0.1.0
admitted manifest digest: sha256:330baced51d67269813678b798b320f9673a85f26e3a59ddb6a918a0471c9c15
target manifest in canonical contract form, digest: sha256:330baced51d67269813678b798b320f9673a85f26e3a59ddb6a918a0471c9c15 (equal: True)
target.manifest == round-tripped admitted manifest: False
differs: manifest.realization_support[0].artifact_mechanisms: list != tuple
differs: manifest.capabilities.provisioner.operating_systems: same items in another order
target: operating_systems (debian, ubuntu, rocky-linux, x-aptl:almalinux, x-aptl:alpine, x-aptl:amazon-linux, x-aptl:centos, x-aptl:kali); artifact_mechanisms is a list
rebuilt: operating_systems (debian, rocky-linux, ubuntu, x-aptl:almalinux, x-aptl:alpine, x-aptl:amazon-linux, x-aptl:centos, x-aptl:kali); artifact_mechanisms is a tuple
check result: ValueError: mixed runtime target manifest differs from the admitted component
The target's manifest is the admitted contract: rendered by backend_manifest_v2_model, it has the admitted digest. The check refuses it because the two internal representations differ.
No test reaches this refusal. The only MixedRuntimeBinding fixture builds each target's manifest with the same backend_manifest_from_v2_model_with_envelope call the check makes (implementations/python/tests/test_issue_1016_mixed_runtime_coordination.py:278, target at :281-290). The check therefore compares the rebuild with itself, and no test expects mixed runtime target manifest differs from the admitted component. The #1344 fixture-provenance audit found the gap (finding MF-2 in the record #1461 proposes).
MixedRuntimeBinding and MixedRuntimeComponent are public raes_runtime exports (implementations/python/packages/raes_runtime/__init__.py:15). In this repository only the #1016 tests construct them, and LilRAE does not yet bind its target into a mixed composition. Any backend whose own manifest keeps its operating-system rows in an unsorted order, or holds one of these sequences as a list, is refused.
Reproduction script (run with the rae project environment's Python and PYTHONPATH=<lilrae>/src)
import dataclasses
import sys
from aptl.backends.raes_manifest import create_aptl_manifest, create_aptl_realization_envelope
from raes_backend_protocols.manifest import (
backend_manifest_from_v2_model_with_envelope,
backend_manifest_payload,
backend_manifest_v2_model,
)
from raes_backend_stubs.stubs import create_stub_components
from raes_contracts.canonical import canonical_json_digest
from raes_contracts.contracts import BackendManifestV2Model
from raes_contracts.contracts.mixed_composition import MixedCompositionComponentModel
from raes_runtime.mixed_runtime import MixedRuntimeComponent, _require_bound_component
from raes_runtime.registry import RuntimeTarget
def differences(left: object, right: object, path: str = "manifest") -> list[str]:
if dataclasses.is_dataclass(left) and type(left) is type(right):
found: list[str] = []
for item in dataclasses.fields(left):
found += differences(getattr(left, item.name), getattr(right, item.name), f"{path}.{item.name}")
return found
if isinstance(left, tuple | list) and isinstance(right, tuple | list) and len(left) == len(right):
notes = [f"{type(left).__name__} != {type(right).__name__}"] if type(left) is not type(right) else []
if list(left) != list(right) and all(item in right for item in left):
notes.append("same items in another order")
elif list(left) != list(right):
found = []
for index, (a, b) in enumerate(zip(left, right, strict=True)):
found += differences(a, b, f"{path}[{index}]")
return found
return [f"{path}: {', '.join(notes)}"] if notes else []
if left != right:
return [f"{path}: {left!r} != {right!r}"]
return []
def main() -> int:
manifest = create_aptl_manifest() # what create_aptl_runtime_target() puts on its RuntimeTarget
envelope = create_aptl_realization_envelope()
admitted = BackendManifestV2Model.model_validate(backend_manifest_payload(manifest)) # LilRAE's published payload
digest = canonical_json_digest(admitted.model_dump(mode="json"))
declaration = MixedCompositionComponentModel(
component_id="aptl",
apparatus_identity_ref="apparatus:aptl",
realization_form="emulation-or-operation",
manifest_ref={
"ref_kind": "manifest",
"ref_id": admitted.identity.name,
"ref_version": admitted.schema_version,
"ref_digest": digest,
"subject_ref": {
"ref_kind": "backend",
"ref_id": admitted.identity.name,
"ref_version": admitted.identity.version,
},
},
realization_envelope=envelope.identity,
)
parts = create_stub_components(manifest=manifest)
target = RuntimeTarget(
name="aptl",
manifest=manifest,
provisioner=parts.provisioner,
orchestrator=parts.orchestrator,
evaluator=parts.evaluator,
participant_runtime=parts.participant_runtime,
time_runtime=parts.time_runtime,
observation_runtime=parts.observation_runtime,
)
print(f"backend identity: {manifest.identity.name} {manifest.identity.version}")
print(f"admitted manifest digest: {digest}")
rendered = canonical_json_digest(backend_manifest_v2_model(manifest).model_dump(mode="json"))
print(f"target manifest in canonical contract form, digest: {rendered} (equal: {rendered == digest})")
expected = backend_manifest_from_v2_model_with_envelope(admitted, envelope)
print(f"target.manifest == round-tripped admitted manifest: {manifest == expected}")
for line in differences(manifest, expected):
print(f" differs: {line}")
for label, value in (("target", manifest), ("rebuilt", expected)):
rows = ", ".join(row.distribution for row in value.provisioner.operating_systems)
mechanisms = type(value.realization_support[0].artifact_mechanisms).__name__
print(f" {label}: operating_systems ({rows}); artifact_mechanisms is a {mechanisms}")
try:
_require_bound_component(MixedRuntimeComponent(target=target, manifest=admitted, envelope=envelope), declaration)
except ValueError as exc:
print(f"check result: ValueError: {exc}")
return 1
print("check result: accepted")
return 0
if __name__ == "__main__":
sys.exit(main())The stub components only satisfy RuntimeTarget's shape checks; the check reads the target's manifest alone.
Existing Surface Audit
- Admission. Trial admission pins each composition component to the digest of its concrete payload,
canonical_json_digest(manifest.model_dump(mode="json"))(implementations/python/packages/raes_processor/trial_compiler/apparatus.py:84-85, through_resolve_composition_componentsat:348-386), and to its envelope identity. The binding's first comparison re-checks the configured model against that digest. - Canonical form.
backend_manifest_v2_model(implementations/python/packages/raes_backend_protocols/manifest.py:84-257) renders an internal manifest as the authoritative v2 contract model. It sorts the set-valued fields and the operating-system rows, and lists contract ids in authority order. Other checks identify a target's manifest through it:_require_preparation_contract(implementations/python/packages/raes_runtime/backend_preparation.py:177-179),preparation_authority(implementations/python/packages/raes_processor/planner/realization_preparation.py:47) and the materialization binding (implementations/python/packages/raes_processor/planner/materialization_admission.py:98) each comparecanonical_json_digest(backend_manifest_v2_model(manifest).model_dump(mode="json")). - Envelope. The v2 form carries only the envelope's identity (
manifest.py:121-125).BackendRealizationEnvelopeModelchecks its digest against its content when it is validated (implementations/python/packages/raes_contracts/realization_envelope_carrier.py:286-313), and contract models are not frozen (implementations/python/packages/raes_contracts/_base.py:8-11). Today's dataclass equality also compares the full envelope the target holds. - Decision record. The Coordinate mixed participant runtimes fail-closed #1016 preflight asks the binding to "validate every component target's manifest identity and digest, realization-envelope membership, required runtime component, and current profile-relative support against the sealed profile" (
docs/decisions/issue-1016-mixed-runtime-coordination-preflight.md:49-52). The reference page says the control plane accepts a binding only when its component manifests, realization envelopes and runtime targets "match exactly" (docs/explain/reference/mixed-participant-composition.md:80-83). - Requirement. SEM-234 traces
raes_runtime/mixed_runtime.py(docs/requirements/SEM-234/requirement.md:43). - Tests. Besides the Coordinate mixed participant runtimes fail-closed #1016 module,
implementations/python/tests/test_issue_1355_mixed_mapping_time.pybuilds its bindings through the same_admitted_bindinghelper. - Size.
mixed_runtime.pyhas 407 lines, against the 500-line cap.
Alternatives
- Normalize the sequences in the protocol types, for example sort
operating_systemsinto a tuple inProvisionerCapabilitiesand makeartifact_mechanismsa tuple inRealizationSupportDeclaration: rejected. It changes types that every backend constructs, and the check would still depend on the internal representation of every field added later. - Make the rebuild reproduce the producer's representation: rejected. The published payload sorts the rows, so the authored order is gone, and the container type is the producer's choice.
- Compare the target's rendered manifest, or its digest, with the configured payload as given: rejected. backend-manifest-v2 does not require sorted lists, so a payload that lists its rows in another order would still be refused, although it is the same contract.
- Render both the target's manifest and the rebuilt admitted manifest with
backend_manifest_v2_modeland compare the two, keep the rebuild, which still validates the admitted payload and its envelope join, and compare the target's envelope with the admitted envelope directly: selected.
Chosen Architecture
_require_bound_componentkeeps both refusals and their messages. Its second comparison requires the target's manifest and the rebuilt admitted manifest to be equal once each is rendered bybackend_manifest_v2_model, and the target's realization envelope to equal the admitted envelope.- Every field the contract carries is still compared. Only the internal order and container type stop mattering, where the contract form sorts or lists them. A target whose contract differs from the admitted one, such as one with an operating-system row fewer, is still refused.
- No contract, schema, trial admission or backend changes. LilRAE needs no change.
- The change edits package code bound by the research evidence, so both captures are republished at the next free releases.
Verification Plan
- Commit a capture of LilRAE's manifest as test data,
implementations/python/tests/data/boundary_captures/lilrae-aptl-backend-manifest.json, produced by LilRAE's own functions ata5833df9and holding its provenance. It records the published payload, the realization envelope, and how the producer's own manifest orders its operating-system rows and holds its artifact mechanisms. - A new test module,
implementations/python/tests/test_issue_<this issue>_mixed_runtime_canonical_manifest.py, binds:- a target manifest rebuilt from the capture in the producer's representation, which must be accepted (this case fails on
dev); - the round-trip target that the Coordinate mixed participant runtimes fail-closed #1016 tests build, which must stay accepted;
- a target whose contract differs from the admitted one, which must still be refused with today's message.
- a target manifest rebuilt from the capture in the producer's representation, which must be accepted (this case fails on
- Run
test_issue_1016_mixed_runtime_coordination.py,test_issue_1355_mixed_mapping_time.pyand the new module with default markers and with-m integration. - Rerun the reproduction above against the fix.
- Run
nox -s verify-fast-feedback -- --base-rev origin/dev,nox -s lint,make policy, both research evidence checkers and the three evidence test modules.
Activity
fix(runtime): refuse a target manifest without a v2 contract form as …
test: republish research evidence for issue #1482
docs(research): cite the issue or pull request that tracks each regis…