Reproduction steps:
- Run Station v0.97 with a synthetic Team approval request requiring quorum=2.
- Deliver two valid approvals from two distinct approver keys for the exact
request and action hash. This changes the stored request to approved.
- Deliver a third valid deny from another eligible approver for the same
request and action hash.
- Call the normal live
team_approval.gate()and run a harmless HTTP effect
through the workflow engine with a mock executor.
- Controls: run
deny → approve → approveandapprove → deny → approve,
and verify duplicate approvals from one signer are still de-duplicated.
Expected:
The implementation policy comment says “any explicit deny fails the request”.
Therefore a valid deny received before execution must make the request denied,
or at minimum prevent proceed, even if the quorum was reached earlier.
Actual:
After quorum is reached, the request is marked approved. A subsequent valid
deny is silently ignored because responses are discarded once the record is no
longer pending:
approve → approve → deny:
status=approved, approvals=2, denials=0
gate=proceed
run=COMPLETED
mock_calls=["https://approved.example/hook"]
Controls behave differently and correctly:
deny → approve → approve: status=denied
approve → deny → approve: status=denied
duplicate signer control: duplicate approval counted once
All signatures, request IDs, action hashes, and signer membership checks are
valid. No real network request or financial write was performed.
Station version (railcall version):
station-v0.97
Affected code path:
workbench/primitives/team_approval.py, _handle_incoming_response()
(approximately lines 372–432) returns early when the stored status is notpending. It sets status="approved" immediately when the approval count
reaches quorum (approximately lines 426–427); the deny branch is only reached
while status is still pending. gate() then trusts the approved status without
checking later valid denials.
Security / integrity impact:
A valid approver veto can arrive before execution yet fail to stop a governed
external effect. The resulting approval record and execution outcome do not
reflect the explicit deny, weakening the Team approval guarantee.
Counter-evidence checked:
- Denies before quorum block deterministically in both control orders.
- Duplicate-signer checks work; this is not a duplicate-key issue.
- The post-quorum deny has a valid signature and matching action/request hash.
- No downstream gate or workflow-engine check reprocesses ignored denials.
- The source policy comment explicitly requires any explicit deny to fail the
request; this is not inferred solely from a field name.
Suggested fix:
Keep the request in a state that can record valid denials until execution is
irreversibly committed, or re-check all received signed responses before
returning proceed. A valid deny must transition the request to denied and
invalidate any pending approval block.
AFFECTED VERSION:
station-v0.97