feat(risk): add evidence-backed gate assessment - #285
Conversation
Co-Authored-By: Codex <noreply@openai.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 18fb5b04a7
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Co-Authored-By: Codex <noreply@openai.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 69e838348a
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| ): | ||
| return {}, {"invalid_mandate"} | ||
| authority_scope = mandate_provenance["authority_scope"] | ||
| receipt_sha256 = _sha256(mandate_provenance["authority_receipt_sha256"]) |
There was a problem hiding this comment.
Verify the mandate receipt before applying its limits
When mandate provenance can be altered or supplied by an untrusted caller, assess_with_evidence() accepts any 64-character lowercase hex string as the authority receipt and never checks that it corresponds to the mandate fields being enforced. A caller can therefore retain a valid-looking receipt while raising effective_exposure_cap, expanding allowed_nonzero_assets, or changing product factors, and the gate will issue an approval using authority that the referenced receipt never granted; resolve the receipt from a trusted source or verify it against a canonical mandate payload before using those limits.
Useful? React with 👍 / 👎.
| decision_digest_sha256=_canonical_digest(decision_payload), | ||
| portfolio_snapshot_digest_sha256=_canonical_digest(snapshot_payload), |
There was a problem hiding this comment.
Bind every risk-engine input to the assessment
When configured plugins inspect portfolio positions, snapshot metadata, or market_data, the receipt cannot prove which inputs they assessed: the snapshot digest covers only as_of, exposure, and equity, while no market-data digest is recorded at all. An approval produced from safe holdings or market conditions can consequently be paired with different inputs that the same plugin would reject, yet all recorded input digests still match; hash the full canonical engine inputs or pass only the already-digested normalized payload to the engine.
Useful? React with 👍 / 👎.
| for budget in decision.budgets or (): | ||
| amount = _finite_number(getattr(budget, "amount", None)) | ||
| if amount is None or amount < 0.0: | ||
| return {"invalid_decision_budget"} | ||
| requested_budget += amount |
There was a problem hiding this comment.
Normalize budget units before comparing loss authority
When a decision uses any BudgetIntent.unit other than the mandate's implicit loss-budget unit, _budget_authority_errors() ignores the unit and compares the raw amount directly. For example, a mandate with a 100-quote-currency loss budget approves BudgetIntent(amount=1, unit="BTC"), potentially authorizing far more than the mandate permits; normalize supported units to a common denomination or reject budgets whose unit cannot be matched to the mandate.
Useful? React with 👍 / 👎.
| if weight > min(product_cap, nominal_cap): | ||
| reason_codes.add("product_exposure_cap") |
There was a problem hiding this comment.
Allow floating-point tolerance at product caps
When a target is computed by ordinary floating-point arithmetic at exactly the authorized product cap, this strict comparison can reject it spuriously. For example, 0.1 + 0.2 produces 0.30000000000000004, so a mathematically 30% target is rejected against a 30% product cap even though the overall exposure checks below already use a 1e-9 tolerance; apply the same tolerance here.
Useful? React with 👍 / 👎.
| risk_action = build_risk_engine().assess( | ||
| decision, | ||
| portfolio_snapshot, | ||
| market_data=market_data, | ||
| ) |
There was a problem hiding this comment.
Preserve risk-reduced actions in the evidence gate
When a configured plugin produces ROUTE_RISK_REDUCED, the checked RiskEngine flow resolves it to a risk_reduced action with a 0.5 scalar, but RiskEngine.assess() then collapses every action except blocked and risk_off into approve. Calling that wrapper here therefore approves and returns the original full-size decision instead of rejecting or scaling it; preserve the resolved action so this gate can enforce its non-approve check.
Useful? React with 👍 / 👎.
Summary
qsl.risk_gate_assessment.v1receipts for MEMBER and ACCOUNT gatesValidation
PYTHONPATH=src python3 -m pytest -q(786 passed, 1 skipped)PYTHONPATH=src python3 -m ruff check .PYTHONPATH=src python3 -m compileall -q srcpython3 -m build --no-isolation