Skip to content

Commit d02437c

Browse files
vilenariosclaude
andauthored
lane 4A follow-up #2: classify non-dict verify_commitment input as unsupported (not legacy) (#22)
* 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> * verify_signature: propagate verify_commitment's precise reason field Knock-on of the malformed-input label cleanup: ``verify_signature`` was hardcoding ``reason="unsupported_spec_version"`` whenever ``spec_status`` came back as ``"unsupported"``, which now overwrites the more specific ``"envelope_not_a_json_object"`` diagnostic that ``verify_commitment`` emits for non-dict input. The fix is a one-line change — fall back to the generic reason only when ``verify_commitment`` didn't supply one. Regression test pins ``verify_signature(None, engine)["reason"]``. Suite: 230 passed, 21 skipped. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 2d858ea commit d02437c

3 files changed

Lines changed: 63 additions & 7 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

ario_mlflow/verify.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -479,7 +479,11 @@ def verify_signature(envelope: dict, proof_engine: ProofEngine) -> dict:
479479
"legacy_envelope": legacy,
480480
}
481481
if spec_status == "unsupported":
482-
out["reason"] = "unsupported_spec_version"
482+
# Propagate the more precise diagnostic from verify_commitment when
483+
# available (e.g. ``envelope_not_a_json_object`` for malformed input);
484+
# fall back to the generic ``unsupported_spec_version`` when the
485+
# envelope had a real-but-unknown spec major.
486+
out["reason"] = result.get("reason", "unsupported_spec_version")
483487
return out
484488

485489

tests/test_plugin_smoke.py

Lines changed: 40 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"))
@@ -419,6 +442,23 @@ def test_verify_signature_wrapper_surfaces_spec_version_state(tmp_path):
419442
assert out["reason"] == "unsupported_spec_version"
420443

421444

445+
def test_verify_signature_surfaces_specific_reason_for_non_dict_input(tmp_path):
446+
"""``verify_signature`` should pass the precise diagnostic from
447+
``verify_commitment`` through (``envelope_not_a_json_object`` for
448+
malformed input) rather than always overwriting with the generic
449+
``unsupported_spec_version``. ``ok=False`` is the safety guarantee;
450+
the precise ``reason`` is for diagnostics."""
451+
from ario_mlflow.verify import verify_signature
452+
453+
engine = ProofEngine(str(tmp_path / "priv"), str(tmp_path / "pub"))
454+
out = verify_signature(None, engine)
455+
assert out["ok"] is False
456+
assert out["signature_valid"] is False
457+
assert out["spec_version_status"] == "unsupported"
458+
assert out["legacy_envelope"] is False
459+
assert out["reason"] == "envelope_not_a_json_object"
460+
461+
422462
def test_verify_commitment_rejects_underscore_injection_on_agent_envelope(tmp_path):
423463
"""Profile-conditional ``_*`` strip: only mlflow envelopes are lenient.
424464

0 commit comments

Comments
 (0)