Skip to content

Add fail-closed operational decision gates - #12

Open
Deathcharge wants to merge 8 commits into
mainfrom
codex/decision-plans
Open

Add fail-closed operational decision gates#12
Deathcharge wants to merge 8 commits into
mainfrom
codex/decision-plans

Conversation

@Deathcharge

@Deathcharge Deathcharge commented Aug 1, 2026

Copy link
Copy Markdown
Owner

Summary

  • add a zero-dependency operational decision-gate layer over existing consensus results
  • support explicit pass choices, minority vetoes, closed vocabularies, required participants, and minimum successful weight
  • return deterministic passed, blocked, or indeterminate verdicts with stable reason codes, the applied policy snapshot, and complete consensus evidence
  • add optional versioned policy IDs, schema-v1 serialization, and deterministic SHA-256 policy digests
  • add end-to-end release-gate and policy-panel examples plus API, integration, security, roadmap, and productization documentation
  • close still-valid review feedback from merged PR Productize agent-consensus as a standalone library #4: shared setting validation, response hashing, real metadata serialization coverage, alias normalization, CI action pinning, least-privilege release guidance, and stale evidence wording
  • patch the contributor test runner to pytest 9.0.3, closing GHSA-6w46-j5rx-g56g / CVE-2025-71176 without adding a runtime dependency

Why

The core tally engine was credible but stopped at “what did the group agree on?” Real release, output-safety, authorization, and routing workflows also need a deterministic answer to “may the application act on this evidence?” Keeping policy evaluation separate preserves the small provider-neutral core while giving the package concrete operational use cases.

Blocking evidence takes precedence: a configured veto or agreed non-pass choice is blocked. Missing reviewers, insufficient successful weight, unknown choices, quorum failure, or no consensus is indeterminate. Only a fully satisfied policy is passed.

Read-only inspection of optional Samsarix producer contracts informed the policy-panel recipe. The package imports none of them, so this repository remains independently installable and useful.

Developer impact

New public API:

  • DecisionPolicy
  • DecisionStatus
  • DecisionReason
  • DecisionVerdict
  • evaluate_decision()

DecisionPolicy.digest covers normalized schema-v1 policy content using canonical UTF-8 JSON and SHA-256. A fixed contract vector protects the encoding from drift. The digest is not a signature.

There are still no runtime dependencies, network calls, persistence, provider SDKs, sibling-repository imports, or protected-action side effects. Hosts remain responsible for authenticated identity, authorization, redaction, policy distribution, and enforcement.

Exact-head validation

Head: 37c10fa54afa7261f00ef8eefd6e2d595490a6ec

Fresh Windows Python 3.14.6 environment installed solely from requirements-dev.txt:

  • pytest 9.0.3 confirmed
  • python -m ruff format --check . — passed; 18 files already formatted
  • python -m ruff check . — passed
  • python -m mypy agent_consensus — passed; 6 source files
  • python -m pytest — 79 passed; 99.62% branch-aware coverage
  • python -m build --outdir <fresh-temp>/artifacts — sdist built, then wheel built from the sdist
  • python -m twine check <fresh-temp>/artifacts/* — wheel and sdist passed
  • clean environment pip install --no-deps <wheel> — passed; imported version 0.2.0
  • all seven offline examples executed from the installed wheel — passed, including release and policy-panel gates

Final local artifact snapshot:

  • wheel SHA-256: 3A28A87AF84F0889D2FC925E07183A56A1B6FA4DDCF6CAB3BF568EA7E1CDCFBD
  • sdist SHA-256: 8213BB92E36F7BD9BF7B1B66CFB7CCF37EF2619FCBE4086B6A61C79732F7477B

Hosted CI for the final head passed twice across quality, Linux Python 3.10–3.14, Windows Python 3.14, package build/check, isolated wheel install, and all examples.

Security review

  • Complete Codex Security PR-range scan (daf0dc5..310372a): 5/5 changed source-like rows closed, 0 findings, 0 deferred work.
  • Complete Codex Security incremental scan (310372a..37c10fa): 1/1 source-like row closed, 0 findings, 0 deferred work.
  • GitHub Dependabot alert chore(deps): bump actions/setup-python from 6 to 7 #5 is remediated in this branch by excluding pytest versions below 9.0.3. The alert remains visible on the default branch until merge.
  • Runtime package remains dependency-free; pytest is development-only.

Review and release gates

  • A separate real consumer with a pinned producer version remains the next adoption proof, not a merge blocker for this standalone release candidate.
  • PyPI name ownership, release version/tag, Trusted Publisher setup, and the final MIT/Apache-2.0/MPL-2.0 choice remain owner-controlled.
  • No package was published, no legal license was changed, and no production infrastructure was modified.

@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@Deathcharge, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 47 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: cc345694-7862-4eaf-ab05-f2e720c5262d

📥 Commits

Reviewing files that changed from the base of the PR and between 37c10fa and 64b8118.

📒 Files selected for processing (14)
  • .github/workflows/ci.yml
  • CHANGELOG.md
  • SECURITY.md
  • agent_consensus/__init__.py
  • agent_consensus/core.py
  • agent_consensus/errors.py
  • agent_consensus/models.py
  • agent_consensus/policy.py
  • docs/API_REFERENCE.md
  • docs/INTEGRATIONS.md
  • docs/PRODUCTIZATION.md
  • tests/test_engine.py
  • tests/test_package.py
  • tests/test_policy.py

Summary by CodeRabbit

  • New Features

    • Added fail-closed decision policies with passed, blocked, and indeterminate outcomes.
    • Added auditable policy digests, decision reasons, and stable policy serialization.
    • Added release-gate and policy-panel examples.
    • Exposed decision-policy tools through the public package API.
  • Documentation

    • Added integration, operational gate, API, security, and getting-started guidance.
  • Bug Fixes

    • Improved validation for empty or oversized normalized choices.
    • Improved handling of responses containing unhashable metadata.
  • Chores

    • Updated pytest and strengthened CI action pinning.

Walkthrough

Changes

The package adds immutable, auditable decision policies with fail-closed evaluation. It exports the policy APIs, hardens consensus validation and hashing, adds release-gate examples, and updates integration, release, CI, and product documentation.

Decision policy and release integration

Layer / File(s) Summary
Consensus validation and model hardening
agent_consensus/core.py, agent_consensus/models.py, tests/test_consensus.py, tests/test_engine.py
Shared decision-setting validation is used. Normalizer output must be non-blank and within the size limit. Response metadata is excluded from hashing.
Policy contracts and verdict evaluation
agent_consensus/policy.py, agent_consensus/__init__.py, tests/test_policy.py, tests/test_package.py, docs/API_REFERENCE.md
Adds immutable DecisionPolicy and DecisionVerdict types, stable policy digests, status and reason enums, evaluate_decision, public exports, and comprehensive policy tests.
Release-gate examples and integration guidance
examples/06_release_gate.py, examples/07_policy_panel.py, docs/DECISION_GATES.md, docs/INTEGRATIONS.md, docs/GETTING_STARTED.md, README.md, SECURITY.md, docs/CONSENSUS_ALGORITHMS.md
Adds executable release-gate and policy-panel examples. Documents normalization, vetoes, required participants, fail-closed handling, authorization boundaries, and policy integration.
Release and project verification updates
.github/workflows/ci.yml, CHANGELOG.md, ROADMAP.md, docs/PRODUCTIZATION.md, docs/RELEASING.md, pyproject.toml, requirements-dev.txt
Pins CI actions to commit SHAs, updates pytest to 9.0.3, and records policy functionality, release checks, verification results, and project scope changes.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ReleaseGate
  participant ConsensusEngine
  participant evaluate_decision
  participant DecisionPolicy
  ReleaseGate->>ConsensusEngine: gather weighted participant responses
  ConsensusEngine-->>ReleaseGate: return ConsensusResult
  ReleaseGate->>evaluate_decision: evaluate consensus
  evaluate_decision->>DecisionPolicy: apply policy constraints
  evaluate_decision-->>ReleaseGate: return DecisionVerdict
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 34.78% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed The description clearly explains the decision-gate implementation, supporting changes, validation, and security impact.
Title check ✅ Passed The title clearly and concisely summarizes the main change: adding fail-closed operational decision gates.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/decision-plans

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@Deathcharge
Deathcharge marked this pull request as ready for review August 11, 2026 03:33

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 12

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/ci.yml:
- Around line 19-20: Set persist-credentials: false on all three
actions/checkout steps in .github/workflows/ci.yml at lines 19-20, 44-45, and
58-59; the actions/setup-python step at lines 19-20 requires no change.

In `@agent_consensus/models.py`:
- Around line 281-293: Update _validate_decision_settings to accept a
caller-facing quorum_field label and use it in both min_successful validation
error messages. Pass quorum_field="min_votes" from evaluate_votes while
preserving the existing label for the other public entry point.

In `@agent_consensus/policy.py`:
- Around line 278-279: Update the minimum-weight check in the policy evaluation
logic to use math.isclose with the existing tolerance pattern, treating
successful_weight as meeting min_successful_weight when equal within
floating-point tolerance; only append SUCCESSFUL_WEIGHT_BELOW_MINIMUM when it is
genuinely below the threshold.
- Around line 131-136: Parenthesize the combined set expression in the
allowed_choices validation within the configuration checks, making the subset
comparison explicitly evaluate (pass_choices | veto_choices) against
allowed_choices while preserving the existing error behavior.
- Around line 229-232: Update evaluate_decision’s input validation to use an
exception within the package hierarchy, preferably by adding DecisionInputError
in errors.py as a ConsensusError and TypeError subclass, then raising it for
invalid consensus and policy arguments. Preserve existing TypeError
compatibility while ensuring callers catching ConsensusError handle these
rejections.
- Around line 288-298: Replace the any-based membership check in the incomplete
calculation with an intersection against a module-level frozenset of incomplete
DecisionReason values, defined alongside the enums. Preserve the existing reason
set exactly and derive incomplete from whether the intersection with reasons is
non-empty.

In `@docs/API_REFERENCE.md`:
- Around line 102-111: Update the DecisionPolicy signature documentation so
veto_choices and required_participants use empty frozenset defaults, matching
the actual defaults in policy.py; leave pass_choices unchanged.

In `@docs/INTEGRATIONS.md`:
- Around line 32-33: Update docs/INTEGRATIONS.md lines 32-33 to state that
callers must pass panel_normalizer into consensus evaluation before configuring
allowed_choices={"approve", "hold", "reject"}, ensuring aliases normalize before
unknown-value validation. Update SECURITY.md lines 33-34 to replace the claim
that adapters cannot introduce unreviewed choices with wording that an
unreviewed normalized choice cannot pass.
- Around line 41-43: Update the adapter recipes around authorization_review and
ethics_review to define and pass the per-request request/context values
explicitly. Prefer showing a factory or per-run binding that supplies fresh
inputs to each callback, or clearly mark the snippets as illustrative pseudocode
so they do not imply undefined or stale closure state.

In `@docs/PRODUCTIZATION.md`:
- Around line 343-345: Update the CI verification record near the
implementation-head reference to replace the abbreviated hash 4ff499b with the
complete commit SHA, preserving the surrounding job results and
publication-scope details.

In `@tests/test_policy.py`:
- Around line 212-216: Update test_policy_digest_has_a_stable_schema_v1_contract
to document that a digest mismatch must preserve the existing golden value and
treat _content_dict changes as a schema or compatibility migration requiring
deliberate handling of stored verdicts, rather than replacing the expected hash.
- Around line 219-239: Type the test_policy parameter as a zero-argument
Callable returning DecisionPolicy, import Callable from collections.abc, and
remove the type: ignore from policy(). Keep the existing parametrized factories
and invalid-policy assertions unchanged.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: d9c4d74d-c8ce-4ddb-b521-efcd22bc8793

📥 Commits

Reviewing files that changed from the base of the PR and between daf0dc5 and 37c10fa.

📒 Files selected for processing (24)
  • .github/workflows/ci.yml
  • CHANGELOG.md
  • README.md
  • ROADMAP.md
  • SECURITY.md
  • agent_consensus/__init__.py
  • agent_consensus/core.py
  • agent_consensus/models.py
  • agent_consensus/policy.py
  • docs/API_REFERENCE.md
  • docs/CONSENSUS_ALGORITHMS.md
  • docs/DECISION_GATES.md
  • docs/GETTING_STARTED.md
  • docs/INTEGRATIONS.md
  • docs/PRODUCTIZATION.md
  • docs/RELEASING.md
  • examples/06_release_gate.py
  • examples/07_policy_panel.py
  • pyproject.toml
  • requirements-dev.txt
  • tests/test_consensus.py
  • tests/test_engine.py
  • tests/test_package.py
  • tests/test_policy.py

Comment thread .github/workflows/ci.yml
Comment thread agent_consensus/models.py Outdated
Comment on lines +281 to +293
def _validate_decision_settings(threshold: float, min_successful: int) -> None:
"""Validate the decision settings shared by both public entry points."""
if (
isinstance(threshold, bool)
or not isinstance(threshold, (int, float))
or not math.isfinite(threshold)
or not 0 < threshold <= 1
):
raise ConfigurationError("threshold must be greater than 0 and at most 1")
if isinstance(min_successful, bool) or not isinstance(min_successful, int):
raise ConfigurationError("min_successful must be an integer")
if min_successful < 1:
raise ConfigurationError("min_successful must be at least 1")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Report the caller's parameter name in the quorum error message.

evaluate_votes exposes the quorum parameter as min_votes (agent_consensus/core.py Line 169). This validator always raises "min_successful must be an integer". A caller who passes min_votes=0 receives a message that names a parameter absent from the evaluate_votes signature.

Pass the caller-facing label into the validator.

🔧 Proposed fix
-def _validate_decision_settings(threshold: float, min_successful: int) -> None:
+def _validate_decision_settings(
+    threshold: float,
+    min_successful: int,
+    *,
+    quorum_field: str = "min_successful",
+) -> None:
     """Validate the decision settings shared by both public entry points."""
     if (
         isinstance(threshold, bool)
         or not isinstance(threshold, (int, float))
         or not math.isfinite(threshold)
         or not 0 < threshold <= 1
     ):
         raise ConfigurationError("threshold must be greater than 0 and at most 1")
     if isinstance(min_successful, bool) or not isinstance(min_successful, int):
-        raise ConfigurationError("min_successful must be an integer")
+        raise ConfigurationError(f"{quorum_field} must be an integer")
     if min_successful < 1:
-        raise ConfigurationError("min_successful must be at least 1")
+        raise ConfigurationError(f"{quorum_field} must be at least 1")

Then update the call in agent_consensus/core.py:

_validate_decision_settings(threshold, min_votes, quorum_field="min_votes")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def _validate_decision_settings(threshold: float, min_successful: int) -> None:
"""Validate the decision settings shared by both public entry points."""
if (
isinstance(threshold, bool)
or not isinstance(threshold, (int, float))
or not math.isfinite(threshold)
or not 0 < threshold <= 1
):
raise ConfigurationError("threshold must be greater than 0 and at most 1")
if isinstance(min_successful, bool) or not isinstance(min_successful, int):
raise ConfigurationError("min_successful must be an integer")
if min_successful < 1:
raise ConfigurationError("min_successful must be at least 1")
def _validate_decision_settings(
threshold: float,
min_successful: int,
*,
quorum_field: str = "min_successful",
) -> None:
"""Validate the decision settings shared by both public entry points."""
if (
isinstance(threshold, bool)
or not isinstance(threshold, (int, float))
or not math.isfinite(threshold)
or not 0 < threshold <= 1
):
raise ConfigurationError("threshold must be greater than 0 and at most 1")
if isinstance(min_successful, bool) or not isinstance(min_successful, int):
raise ConfigurationError(f"{quorum_field} must be an integer")
if min_successful < 1:
raise ConfigurationError(f"{quorum_field} must be at least 1")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@agent_consensus/models.py` around lines 281 - 293, Update
_validate_decision_settings to accept a caller-facing quorum_field label and use
it in both min_successful validation error messages. Pass
quorum_field="min_votes" from evaluate_votes while preserving the existing label
for the other public entry point.

Comment thread agent_consensus/policy.py
Comment thread agent_consensus/policy.py Outdated
Comment thread agent_consensus/policy.py Outdated
Comment thread docs/INTEGRATIONS.md Outdated
Comment thread docs/INTEGRATIONS.md Outdated
Comment thread docs/PRODUCTIZATION.md
Comment thread tests/test_policy.py
Comment thread tests/test_policy.py Outdated
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant