Skip to content

fix(escalation): record votes in approve/deny; fix quorum always evaluating to 0 - #3127

Closed
Imran Siddique (imran-siddique) wants to merge 1 commit into
mainfrom
fix/escalation-quorum-votes-3126
Closed

Imran Siddique (imran-siddique) wants to merge 1 commit into
mainfrom
fix/escalation-quorum-votes-3126

Conversation

@imran-siddique

Copy link
Copy Markdown
Collaborator

Summary

  • InMemoryApprovalQueue.approve() and deny() were setting req.decision and req.resolved_by but never appending to req.votes
  • EscalationHandler.resolve() evaluates quorum by summing req.votes -- with an always-empty list the count was 0, resetting a correctly-resolved decision back to PENDING and falling through to the timeout default
  • Fix: append the vote tuple (approver, verdict, timestamp) to req.votes before any decision logic; deduplicate by approver so a single reviewer cannot satisfy quorum multiple times

Fixes #3126

Changes

agent-governance-python/agent-os/src/agent_os/integrations/escalation.py

  • approve(): append (approver, "ALLOW", datetime.now(...)) to req.votes before setting decision; skip duplicate votes from the same approver; allow additional votes to be recorded after decision is already final
  • deny(): same pattern for "DENY" verdict

agent-governance-python/agent-os/tests/test_escalation.py

  • Renamed test_double_approve_fails to test_double_approve_same_approver_rejected (clarifies the dedup semantics, not "already resolved")
  • Added test_second_approver_vote_recorded -- second approver's vote appended, both visible in req.votes
  • Added test_votes_recorded_on_approve / test_votes_recorded_on_deny -- assert votes list populated with correct approver and verdict
  • Added TestQuorumResolution class: quorum met resolves ALLOW, quorum not met times out to DENY, duplicate approver vote rejected and does not satisfy quorum

Test plan

  • All 26 escalation tests pass (pytest tests/test_escalation.py)
  • test_quorum_met_resolves_allow: quorum=1, single approver, resolves ALLOW
  • test_quorum_not_met_times_out: quorum=2, 1 approver, times out to DENY
  • test_duplicate_approver_does_not_satisfy_quorum: second call from same approver returns False, votes list length stays at 1

🤖 Generated with Claude Code

@github-actions github-actions Bot added documentation Improvements or additions to documentation dependencies Pull requests that update a dependency file tests agent-mesh agent-mesh package size/XL Extra large PR (500+ lines) labels Jun 21, 2026
@github-actions

github-actions Bot commented Jun 21, 2026 •

Copy link
Copy Markdown
🤖 AI Agent: security-scanner — View details

AI-generated review output. Treat it as untrusted analysis and verify before acting.

No security issues found.

@github-actions

github-actions Bot commented Jun 21, 2026 •

Copy link
Copy Markdown
🤖 AI Agent: docs-sync-checker — Docs Sync

AI-generated review output. Treat it as untrusted analysis and verify before acting.

Docs Sync

  • InMemoryApprovalQueue.approve() in agent-governance-python/agent-os/src/agent_os/integrations/escalation.py -- missing docstring
  • InMemoryApprovalQueue.deny() in agent-governance-python/agent-os/src/agent_os/integrations/escalation.py -- missing docstring
  • README.md -- ensure any behavior changes to quorum evaluation or vote tracking are documented
  • CHANGELOG.md -- missing entry for changes to quorum evaluation and vote tracking logic

@github-actions

github-actions Bot commented Jun 21, 2026 •

Copy link
Copy Markdown
🤖 AI Agent: test-generator — `agent-governance-python/agent-os/src/agent_os/integrations/escalation.py`

AI-generated review output. Treat it as untrusted analysis and verify before acting.

agent-governance-python/agent-os/src/agent_os/integrations/escalation.py

  • test_empty_approver_rejected_on_approve -- Validate that empty or whitespace-only approver names are rejected in approve().
  • test_empty_approver_rejected_on_deny -- Validate that empty or whitespace-only approver names are rejected in deny().
  • test_duplicate_approver_does_not_satisfy_quorum -- Ensure duplicate votes from the same approver do not count towards quorum.
  • test_votes_recorded_on_approve -- Verify that approve() appends correct vote details to req.votes.
  • test_votes_recorded_on_deny -- Verify that deny() appends correct vote details to req.votes.

agent-governance-python/agent-os/tests/test_escalation.py

  • test_quorum_met_resolves_allow -- Ensure quorum is correctly evaluated to resolve an approval.
  • test_quorum_not_met_times_out -- Validate timeout behavior when quorum is not met.
  • test_second_approver_vote_recorded -- Confirm that votes from multiple approvers are recorded correctly.
  • test_double_approve_same_approver_rejected -- Ensure duplicate votes from the same approver are rejected.

Test coverage looks good for the changes made. No additional gaps identified.

@github-actions

github-actions Bot commented Jun 21, 2026 •

Copy link
Copy Markdown
🤖 AI Agent: code-reviewer — View details

AI-generated review output. Treat it as untrusted analysis and verify before acting.

TL;DR: 0 blockers, 1 warning. Changes improve vote tracking and quorum logic but require additional test coverage for edge cases.

# Sev Issue Where
1 Warn Missing test for quorum resolution when quorum is exactly met (edge). agent-os/tests/test_escalation.py and TestQuorumResolution class

Action items:

  1. Add a test case to verify quorum resolution when the number of approvals exactly matches the quorum requirement.
Warnings: fine as follow-up PRs.
Add edge-case test for quorum resolution when quorum is exactly met.

@github-actions

github-actions Bot commented Jun 21, 2026 •

Copy link
Copy Markdown
🤖 AI Agent: breaking-change-detector — API Compatibility

AI-generated review output. Treat it as untrusted analysis and verify before acting.

API Compatibility

Severity Change Impact
High approve() and deny() methods in InMemoryApprovalQueue now require an approver argument to be non-empty and unique for each vote. Existing code that calls these methods without providing a valid approver argument or with duplicate approvers will break.
High approve() and deny() methods now append votes to req.votes and enforce deduplication by approver. Any reliance on the previous behavior (e.g., no vote tracking or allowing duplicate votes) will break.
Medium Behavior of quorum evaluation in EscalationHandler.resolve() has changed due to the enforcement of vote tracking and deduplication. Existing workflows relying on the previous quorum evaluation logic may behave differently.
Medium Tests that directly manipulated req.votes in test_security_hardening.py have been refactored to use the updated approve() and deny() methods. Custom test setups or mocks that relied on direct manipulation of req.votes will need to be updated.

@github-actions

github-actions Bot commented Jun 21, 2026 •

Copy link
Copy Markdown

PR Review Summary

Check Status Details
🔍 Code Review ⚠️ Missing No current-run comment
🛡️ Security Scan ⚠️ Missing No current-run comment
🔄 Breaking Changes ⚠️ Missing No current-run comment
📝 Docs Sync ⚠️ Missing No current-run comment
🧪 Test Coverage ⚠️ Missing No current-run comment

Verdict: ⚠️ AI review incomplete; ready for human review

AI review comments are untrusted advisory output. The summary reports workflow-generated completion status only, not model-authored pass/fail claims.

@github-actions github-actions Bot added size/M Medium PR (< 200 lines) and removed documentation Improvements or additions to documentation dependencies Pull requests that update a dependency file agent-mesh agent-mesh package size/XL Extra large PR (500+ lines) labels Jun 21, 2026
@github-actions

github-actions Bot commented Jun 21, 2026 •

Copy link
Copy Markdown

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Scanned Files

None

@imran-siddique

Copy link
Copy Markdown
Collaborator Author

DCO sign-off is needed on the 8 commits this PR presents to GitHub. The commits themselves are correct; the sign-off is purely a procedural fix.

Fastest path to unblock:

git checkout fix/escalation-quorum-votes-3126
git pull --rebase origin fix/escalation-quorum-votes-3126

# Create clean 2-commit version from main
git checkout -b fix/escalation-quorum-votes-3126-dco main
git cherry-pick -s 6b4788ff  # fix(escalation)
# resolve edu-k12 conflict: just git add examples/policy-templates/edu-k12.yaml
git cherry-pick -s b3904c0b  # fix(tests)
git push origin fix/escalation-quorum-votes-3126-dco:fix/escalation-quorum-votes-3126 --force-with-lease

This reduces the PR to 2 commits, both with Signed-off-by: Imran Siddique <imran.siddique@opaque.co>. The diff stays exactly the same (same 4 files: escalation.py, test_escalation.py, test_security_hardening.py, edu-k12.yaml). The policy gate and CI tests should pass once the edu-k12/quorum fixes in this PR land (they self-bootstrap).

@MohammadHaroonAbuomar MohammadHaroonAbuomar left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The quorum fix itself is correct, idempotent, and thread-safe.

The examples/policy-templates/edu-k12.yaml hunk needs to be dropped from this PR. Main now has newer fixes to the same three regexes (lazy .*?, optional (to\\s+|an?\\s+)?, the extra |information keyword) and merging this would overwrite them. It is also unrelated to the escalation fix.

Separately, this fix surfaces a pre-existing issue worth a follow-up: resolve() wakes on the first vote, runs the quorum check once, and falls through to default_action without waiting the remaining timeout for vote 2 of N (escalation.py:176, :424-431). Not introduced here, but now reachable.

Minor: approver: str = "" default (escalation.py:166,184) means one anonymous vote counts toward quorum. Consider rejecting empty when quorum is in play.

@imran-siddique

Copy link
Copy Markdown
Collaborator Author

Thanks for the detailed review MohammadHaroonAbuomar.

edu-k12.yaml: Dropped from this PR. Reverted to main's version -- the newer regex fixes already in main are preserved.

Empty approver: Added if not approver.strip(): return False to both InMemoryApprovalQueue.approve() and deny(), so a blank identity can never count toward quorum. Updated three existing tests that called approve() without an explicit approver and added test_empty_approver_rejected_on_approve / test_empty_approver_rejected_on_deny.

resolve() timing issue: Acknowledged -- resolve() wakes on the first vote and runs the quorum check once, falling through to default_action without waiting the remaining timeout for additional votes. Not introduced by this PR but now reachable with quorum configured. Opening a follow-up issue.

@imran-siddique

Copy link
Copy Markdown
Collaborator Author

MohammadHaroonAbuomar please merge this when u have time. All tests passing.

… fix quorum (#3126)

Signed-off-by: Imran Siddique <imran.siddique@opaque.co>
@imran-siddique
Imran Siddique (imran-siddique) force-pushed the fix/escalation-quorum-votes-3126 branch from 993115b to 9be72c4 Compare June 26, 2026 20:16
@imran-siddique

Copy link
Copy Markdown
Collaborator Author

MohammadHaroonAbuomar thanks for the review. Addressed your change request: the examples/policy-templates/edu-k12.yaml hunk is dropped entirely, the branch no longer touches that file, so main's newer regex fixes (lazy .*?, (to\s+|an?\s+)?, the |information keyword) are preserved. I also rebuilt the branch as a single signed commit on top of current main, so DCO is green again. Could you re-review when you get a chance?

Jack Batzner (@jackbatzner) Ricky Gummadi (@Ricky-G) the quorum fix is correct-in-substance per Mohammad's review and now CI-clean. The only remaining gate is policy/maintainer-approval, which I can't clear myself, my maintainer approval still isn't registering. Could one of you approve? And if possible, please re-add me to the maintainer group so this stops blocking my PRs.

@imran-siddique

Copy link
Copy Markdown
Collaborator Author

Closing this as superseded. The fix is already present on main: escalation.py, test_escalation.py, and test_security_hardening.py on main are identical to this branch, and the vote recording in approve/deny, the quorum fix, and the empty-approver rejection are all in place (escalation.py:171/175/191/195). No separate merge is needed here. Thanks to the reviewers for the look.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/M Medium PR (< 200 lines) tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: EscalationHandler

4 participants