Timmy companion: fail-closed hostile JSON, structured deltas, source_issue validation, bytecode cleanup
Addresses the four release-critical blockers from Timmy's review of PR #42:
1. Guard enum/allowlist membership against unhashable JSON (lists/dicts) in
form.state, receipt.signed_by, and validate_transition target.
2. Require audience_choice.source_issue to be a positive non-bool integer.
3. Make power/scar/cost deltas structured (before -> after), not prose.
4. Remove tracked .pyc bytecode; prove clean checkout stays clean after tests.
Adds 21 new negative tests across HostileJsonFailClosedTests,
SourceIssueValidationTests, and StructuredDeltaTests.
Based on PR #42 head (0b74302). Vincent retains full authorship of the
canon validator, lifecycle model, and r2 review response; this branch
contains only the integration fixes needed to make the stated security
contract true at hostile-input boundaries.
Refs: #19, PR #42
This commit is contained in:
1 parent
0b743027ba
commit
a3330a4642
5 files changed
+173
-13
No files matched your search
@@ -51,6 +51,16 @@ def _is_nonempty_str(v) -> bool:
|
||||
return isinstance(v, str) and v.strip() != ""
|
||||
|
||||
|
||||
def _is_structured_delta(v) -> bool:
|
||||
"""A receipt delta must bind a before/after change, not just prose."""
|
||||
if not isinstance(v, str):
|
||||
return False
|
||||
if "->" not in v:
|
||||
return False
|
||||
parts = v.split("->", 1)
|
||||
return parts[0].strip() != "" and parts[1].strip() != ""
|
||||
|
||||
|
||||
def _validate_provenance(prov, prefix, errors) -> None:
|
||||
if not isinstance(prov, dict):
|
||||
errors.append(f"{prefix}: provenance must be an object, got {type(prov).__name__}")
|
||||
@@ -74,12 +84,19 @@ def _validate_receipt(receipt, idx, form_ids, signers, errors) -> None:
|
||||
return
|
||||
|
||||
# Exact required string fields (non-empty).
|
||||
for field in ("episode_id", "form_id", "action", "power_delta", "scar_delta", "cost_delta"):
|
||||
for field in ("episode_id", "form_id", "action"):
|
||||
if field not in receipt:
|
||||
errors.append(f"{prefix}: missing '{field}'")
|
||||
elif not _is_nonempty_str(receipt[field]):
|
||||
errors.append(f"{prefix}: '{field}' must be a non-empty string")
|
||||
|
||||
# Structured power/scar/cost deltas: must bind before/after, not just prose.
|
||||
for field in ("power_delta", "scar_delta", "cost_delta"):
|
||||
if field not in receipt:
|
||||
errors.append(f"{prefix}: missing '{field}'")
|
||||
elif not _is_structured_delta(receipt[field]):
|
||||
errors.append(f"{prefix}: '{field}' must bind before/after (e.g. '0 -> 1'), got {receipt[field]!r}")
|
||||
|
||||
# form_id must reference a known form.
|
||||
fid = receipt.get("form_id")
|
||||
if isinstance(fid, str) and fid and fid not in form_ids:
|
||||
@@ -95,11 +112,11 @@ def _validate_receipt(receipt, idx, form_ids, signers, errors) -> None:
|
||||
if "signed_by" not in receipt:
|
||||
errors.append(f"{prefix}: missing 'signed_by'")
|
||||
else:
|
||||
signer = receipt["signed_by"]
|
||||
if signer not in signers:
|
||||
errors.append(
|
||||
f"{prefix}: signed_by '{signer}' is not in the configured allowlist"
|
||||
)
|
||||
signer = receipt.get("signed_by")
|
||||
if not isinstance(signer, str):
|
||||
errors.append(f"{prefix}: 'signed_by' must be a non-empty string")
|
||||
elif signer not in signers:
|
||||
errors.append(f"{prefix}: signed_by '{signer}' is not in the configured allowlist")
|
||||
|
||||
|
||||
def _validate_audience_choice(choice, form_states, form_ids, errors) -> None:
|
||||
@@ -125,6 +142,12 @@ def _validate_audience_choice(choice, form_states, form_ids, errors) -> None:
|
||||
if field in choice and not _is_nonempty_str(choice[field]):
|
||||
errors.append(f"audience_choice: '{field}' must be a non-empty string")
|
||||
|
||||
# source_issue must be a positive non-bool integer.
|
||||
si = choice.get("source_issue")
|
||||
if "source_issue" in choice:
|
||||
if not isinstance(si, int) or isinstance(si, bool) or si <= 0:
|
||||
errors.append("audience_choice: source_issue must be a positive integer")
|
||||
|
||||
# committed_at must be a valid ISO timestamp.
|
||||
if "committed_at" in choice:
|
||||
ts = choice["committed_at"]
|
||||
@@ -232,7 +255,7 @@ def validate_canon(data, signers=None) -> dict:
|
||||
state = form.get("state")
|
||||
if state is None:
|
||||
errors.append(f"{prefix}: missing lifecycle state")
|
||||
elif state not in VALID_STATES:
|
||||
elif not isinstance(state, str) or state not in VALID_STATES:
|
||||
errors.append(f"{prefix}: invalid state '{state}', expected one of {sorted(VALID_STATES)}")
|
||||
else:
|
||||
form_states[fid] = state
|
||||
@@ -295,7 +318,7 @@ def validate_transition(form, new_state) -> dict:
|
||||
current = form.get("state")
|
||||
if not isinstance(current, str) or current not in VALID_TRANSITIONS:
|
||||
return {"valid": False, "errors": [f"invalid current state {current!r}"]}
|
||||
if new_state not in VALID_STATES:
|
||||
if not isinstance(new_state, str) or new_state not in VALID_STATES:
|
||||
return {"valid": False, "errors": [f"invalid target state {new_state!r}"]}
|
||||
if new_state not in VALID_TRANSITIONS[current]:
|
||||
return {"valid": False, "errors": [f"illegal transition: '{current}' -> '{new_state}'"]}
|
||||
|
||||
Reference in new issue
Block a user