Skip to content

Commit e3f1727

Browse files
vilenariosclaude
andcommitted
lane 4A follow-up: classify non-dict input as unsupported, not legacy
Caught during a second adversarial sweep of the kernel adapter: the ``verify_commitment`` wrapper was coercing non-dict input to an empty dict before handing it to the kernel. The kernel's empty-dict branch treats ``{}`` as a legacy envelope (missing ``spec_version``), so non-dict input (``None``, lists, strings, scalars) was producing a result with ``spec_version_status="legacy"`` and ``legacy_envelope=True`` — labels that read as "valid pre-spec_version envelope that failed signature" when the truth is "this isn't a valid envelope shape at all." ``overall=False`` was correct in every case (the safety-relevant guarantee), so this was a cosmetic, not a correctness, issue. But the labels appear in CLI panels, logs, and audit-export JSON, so consistency matters. Two changes: - Pass the envelope through to the kernel unchanged — the kernel handles non-dict directly (returns ``legacy_envelope=False`` with errors ``["envelope is not a JSON object"]``), so the coercion was the bug. - Fix the ``spec_version_status`` synth: non-dict input is ``unsupported``, with ``reason="envelope_not_a_json_object"`` for diagnostics. The trichotomy stays (``supported`` / ``legacy`` / ``unsupported``); only malformed input gets the precise diagnostic ``reason``. Parametrized regression test covers five non-dict shapes (None / [] / "hello" / 42 / True). The pre-kernel in-tree code crashed with ``AttributeError`` on the same inputs, so this also locks in the soft-fail improvement from the kernel migration. Suite: 229 passed, 21 skipped. Cross-product green (``TestCrossProductVerify`` + ``cross-product-verified-model`` E2E). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 2d858ea commit e3f1727

2 files changed

Lines changed: 41 additions & 6 deletions

File tree

ario_mlflow/proof.py

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -216,18 +216,30 @@ def verify_commitment(
216216
"""
217217
# ``allow_legacy=True`` preserves the historical behavior of accepting
218218
# envelopes anchored before ``spec_version`` shipped (the only mlflow
219-
# envelopes on Arweave that lack the field).
219+
# envelopes on Arweave that lack the field). The kernel handles
220+
# non-dict input directly (returns a fully-failed VerificationResult
221+
# with ``legacy_envelope=False``), so pass it through unchanged
222+
# instead of coercing — coercing non-dict → ``{}`` would land in the
223+
# kernel's empty-dict-is-legacy branch and produce ``legacy_envelope
224+
# =True``, which misclassifies malformed input as a legacy envelope.
220225
result = _kernel_verify_envelope(
221-
envelope if isinstance(envelope, dict) else {},
226+
envelope,
222227
payload_bytes=payload_bytes,
223228
allow_legacy=True,
224229
)
225230

231+
is_dict = isinstance(envelope, dict)
232+
spec_version = envelope.get("spec_version") if is_dict else None
233+
226234
# Synthesize the trichotomy from the kernel's binary signal + legacy
227235
# flag. ``spec_version_status`` is part of the mlflow result contract;
228236
# callers (verify_signature, tests, the CLI report) branch on it.
229-
spec_version = (envelope or {}).get("spec_version") if isinstance(envelope, dict) else None
230-
if spec_version is None:
237+
# Non-dict input is malformed, not legacy: label it ``unsupported``
238+
# so the result reads coherently (e.g. ``spec_status=="unsupported"``
239+
# never appears alongside ``legacy_envelope=True``).
240+
if not is_dict:
241+
spec_status = "unsupported"
242+
elif spec_version is None:
231243
spec_status = "legacy"
232244
elif result.spec_version_ok:
233245
spec_status = "supported"
@@ -240,11 +252,11 @@ def verify_commitment(
240252
"signature_valid": result.signature_ok,
241253
"payload_hash_valid": result.payload_hash_ok,
242254
"computed_payload_hash": computed,
243-
"stored_payload_hash": (envelope or {}).get("payload_hash") if isinstance(envelope, dict) else None,
255+
"stored_payload_hash": envelope.get("payload_hash") if is_dict else None,
244256
"spec_version_status": spec_status,
245257
"legacy_envelope": result.legacy_envelope,
246258
"overall": result.ok,
247259
}
248260
if spec_status == "unsupported":
249-
out["reason"] = "unsupported_spec_version"
261+
out["reason"] = "unsupported_spec_version" if is_dict else "envelope_not_a_json_object"
250262
return out

tests/test_plugin_smoke.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,29 @@ def test_verify_commitment_ignores_underscore_prefixed_caller_annotations(tmp_pa
269269
assert engine.verify_commitment(env_tampered)["signature_valid"] is False
270270

271271

272+
@pytest.mark.parametrize("malformed", [None, [], "hello", 42, True])
273+
def test_verify_commitment_classifies_non_dict_as_unsupported(tmp_path, malformed):
274+
"""Adversarial / malformed input (non-dict) must classify cleanly.
275+
276+
``overall=False`` is the safety-relevant guarantee, but the label
277+
fields should also read coherently: a non-dict input is malformed,
278+
not legacy. ``spec_version_status="unsupported"`` and
279+
``legacy_envelope=False`` together describe "this isn't a valid
280+
envelope shape," with ``reason="envelope_not_a_json_object"``
281+
surfaced for diagnostics. The pre-kernel in-tree code crashed with
282+
``AttributeError`` on non-dict input; the kernel-backed adapter
283+
soft-fails uniformly.
284+
"""
285+
engine = ProofEngine(str(tmp_path / "priv"), str(tmp_path / "pub"))
286+
result = engine.verify_commitment(malformed)
287+
288+
assert result["overall"] is False
289+
assert result["signature_valid"] is False
290+
assert result["spec_version_status"] == "unsupported"
291+
assert result["legacy_envelope"] is False
292+
assert result.get("reason") == "envelope_not_a_json_object"
293+
294+
272295
def test_create_commitment_event_id_and_signed_at_overrides(tmp_path):
273296
"""Caller may provide event_id / signed_at for deterministic tests."""
274297
engine = ProofEngine(str(tmp_path / "priv"), str(tmp_path / "pub"))

0 commit comments

Comments
 (0)