Reproduction steps:
- Mint a valid signed team manifest (team_manifest.mint_root() +
mint_manifest(), one owner member).
- Build an approval block: schema=1, matching action_hash and team_id,
approvals: [] (zero signatures), quorum=-1.
- Call primitives/team_approval.py::verify_approval_block(action_hash,
block, manifest_doc).
- Repeat with quorum="0" (string).
Expected: any quorum below 1, and any block with zero valid approval
signatures, must be rejected.
Actual:
quorum=1 -> ok=False, "only 0 valid approval(s), quorum is 1" (control, correct)
quorum=2 -> ok=False, "only 0 valid approval(s), quorum is 2" (control, correct)
quorum=0 -> ok=False, "only 0 valid approval(s), quorum is 1" (int 0 is falsy, or 1 saves it)
quorum=-1 -> ok=True, "ok" (BYPASS)
quorum="0" -> ok=True, "ok" (BYPASS — string "0" is truthy)
Root cause: workbench/primitives/team_approval.py:483 —
quorum = int(block.get("quorum") or 1)
if len(seen) < quorum: return False, ...x or 1 only rescues a falsy int 0; a negative int or a truthy non-empty
string like "0" pass through unchanged, then int(-1) or int("0") produce a
quorum the empty-approvals count (0) is not "less than." The identical
pattern exists in the CLI-side verifier (~/.railcall/railcall_cli.py, line
2092: quorum = int(block.get("quorum") or 1)), so both verification paths
share the bug.
Suggested fix:
quorum = int(block.get("quorum") or 1)
if quorum < 1:
return False, f"invalid quorum {quorum!r} — must be >= 1"
if len(seen) < quorum:
...
(apply the same guard in railcall_cli.py's verifier.)
Station version (railcall version): station-v0.69
Module slug + version: Not module-specific; Teams receipt/approval verification
(primitives/team_approval.py::verify_approval_block, and railcall_cli.py
duplicate)