feat(governance): enforce pre-action spend caps#314
Conversation
Signed-off-by: Gnani Rahul Nutakki <gnani.nutakki@gmail.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (10)
🚧 Files skipped from review as they are similar to previous changes (9)
📝 WalkthroughWalkthroughAdds pre-action token and monetary spend authority with signed passport claims, immutable quotes, durable reservations, conservative settlement/quarantine, proxy enforcement, receipt schemas, metrics, tests, and supporting ADR/reference documentation. ChangesSpend budget governance
Estimated code review effort: 5 (Critical) | ~120 minutes Sequence Diagram(s)sequenceDiagram
participant Adapter
participant GovernanceProxy
participant StaticSpendQuoteStore
participant FileSpendBudgetLedger
participant ProviderExecutor
participant ReceiptLog
Adapter->>GovernanceProxy: evaluate_tool_call(spend_request)
GovernanceProxy->>StaticSpendQuoteStore: resolve quote snapshot
GovernanceProxy->>FileSpendBudgetLedger: reserve token and currency bounds
FileSpendBudgetLedger-->>GovernanceProxy: permit or denial
GovernanceProxy->>ProviderExecutor: execute permitted call
ProviderExecutor-->>GovernanceProxy: usage evidence
GovernanceProxy->>FileSpendBudgetLedger: settle or quarantine reservation
GovernanceProxy->>ReceiptLog: append signed lifecycle receipt
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
Signed-off-by: Gnani Rahul Nutakki <gnani.nutakki@gmail.com>
|
CI repair pushed in signed commit ccfd67f. Root cause and repair:
Targeted local verification:
Fresh CI is running against ccfd67f. |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 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 `@docs/reference/spend-budgets.md`:
- Around line 1-204: Regenerate the generated documentation counterpart for the
page using the repository’s source-doc synchronization step, ensuring the output
reflects the current spend-budget content. Verify the result with
site/scripts/sync_source_docs.py --check and commit the generated file.
🪄 Autofix (Beta)
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: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: fb593a97-dd5c-4a2c-ac4d-7ce7cbe1f7e3
📒 Files selected for processing (15)
README.mddocs/decisions/ADR-025-pre-action-spend-reservation.mddocs/decisions/README.mddocs/reference/README.mddocs/reference/spend-budgets.mddocs/specs/execution-receipt-v0.2.schema.jsonpython/README.mdpython/tests/test_spend_budget.pypython/vibap/__init__.pypython/vibap/_specs/execution_receipt_v02.schema.jsonpython/vibap/metrics.pypython/vibap/passport.pypython/vibap/proxy.pypython/vibap/receipt.pypython/vibap/spend_budget.py
👮 Files not reviewed due to content moderation or server errors (8)
- python/vibap/spend_budget.py
- python/vibap/passport.py
- docs/specs/execution-receipt-v0.2.schema.json
- python/vibap/_specs/execution_receipt_v02.schema.json
- python/vibap/receipt.py
- python/vibap/init.py
- python/vibap/proxy.py
- python/vibap/metrics.py
| # Pre-action spend budgets | ||
|
|
||
| Ardur's Python library can reserve token and monetary authority before a | ||
| metered tool or provider call. This surface is separate from | ||
| `max_tool_calls`: call counts, token quantities, and money are never converted | ||
| implicitly. | ||
|
|
||
| ## Policy claim | ||
|
|
||
| Pass `spend_budget` when constructing a `MissionPassport`: | ||
|
|
||
| ```python | ||
| from vibap import MissionPassport | ||
|
|
||
| mission = MissionPassport( | ||
| agent_id="report-agent", | ||
| mission="Generate the approved report", | ||
| allowed_tools=["llm_generate"], | ||
| resource_scope=["**"], | ||
| spend_budget={ | ||
| "version": 1, | ||
| "currency": "USD", | ||
| "metered_tools": ["llm_generate"], | ||
| "ceilings": { | ||
| "tokens": { | ||
| "session": 100_000, | ||
| "agent": 200_000, | ||
| "lineage": 500_000, | ||
| }, | ||
| "currency_micros": { | ||
| "session": 2_000_000, | ||
| "agent": 4_000_000, | ||
| "lineage": 10_000_000, | ||
| }, | ||
| }, | ||
| }, | ||
| ) | ||
| ``` | ||
|
|
||
| All amounts must be non-negative integers no greater than | ||
| `9007199254740991`. `currency_micros` means one millionth of the declared | ||
| three-letter uppercase currency. Issuance adds `lineage_id`; mission authors | ||
| must not invent a different lineage identity for a child. Derived children | ||
| inherit the exact policy and share the root lineage ceiling. | ||
|
|
||
| Only tools named in `metered_tools` require a spend request. Supplying a spend | ||
| request for an unmetered tool fails closed so an adapter cannot mistakenly | ||
| believe an unenforced request was reserved. | ||
|
|
||
| ## Operator quote snapshots | ||
|
|
||
| Quotes are immutable local configuration. They bind a quote ID to one exact | ||
| tool, model, currency, validity interval, and integer input/output rates: | ||
|
|
||
| ```python | ||
| import time | ||
|
|
||
| from vibap import GovernanceProxy, SpendQuote, StaticSpendQuoteStore | ||
|
|
||
| now = int(time.time()) | ||
| quote_store = StaticSpendQuoteStore([ | ||
| SpendQuote( | ||
| quote_id="approved-model-2026-07", | ||
| tool_name="llm_generate", | ||
| model="approved-model-v1", | ||
| currency="USD", | ||
| input_micros_per_million_tokens=1_000_000, | ||
| output_micros_per_million_tokens=2_000_000, | ||
| valid_from=now, | ||
| valid_until=now + 86_400, | ||
| ) | ||
| ]) | ||
|
|
||
| proxy = GovernanceProxy(spend_quote_store=quote_store) | ||
| ``` | ||
|
|
||
| The values above demonstrate the contract; they are not provider prices. | ||
| Operators own price sourcing, review, rotation, and validity intervals. Ardur | ||
| does not fetch pricing over the network and does not accept a rate or currency | ||
| from the caller or provider response. | ||
|
|
||
| ## Reserve before execution | ||
|
|
||
| The adapter provides bounded usage intent, not pricing authority: | ||
|
|
||
| ```python | ||
| from vibap import Decision, SpendReservationRequest | ||
|
|
||
| request = SpendReservationRequest( | ||
| request_id="adapter-idempotency-key", | ||
| quote_id="approved-model-2026-07", | ||
| model="approved-model-v1", | ||
| max_input_tokens=4_000, | ||
| max_output_tokens=2_000, | ||
| ) | ||
|
|
||
| decision, reason = proxy.evaluate_tool_call( | ||
| session, | ||
| "llm_generate", | ||
| {"input_digest": "adapter-owned-bounded-reference"}, | ||
| spend_request=request, | ||
| ) | ||
|
|
||
| if decision != Decision.PERMIT: | ||
| # Do not invoke the provider. | ||
| raise PermissionError(reason) | ||
|
|
||
| response = provider.generate(...) | ||
| ``` | ||
|
|
||
| The ledger atomically checks token and money at session, agent, and lineage | ||
| scopes. A request exceeding any scope is denied before `PERMIT`. Reusing an | ||
| active request ID with the same fingerprint is also denied to prevent a second | ||
| provider execution; reusing it with different semantics is a conflict. Every | ||
| close operation is bound to the session that created the reservation, so a | ||
| sibling session cannot settle or quarantine it. | ||
|
|
||
| If evaluation raises after the ledger accepted a reservation but before | ||
| returning, the proxy attempts an idempotent internal release. A failed release | ||
| raises `spend_compensation_failed` and deliberately retains the reservation; | ||
| operators must treat that state as unresolved rather than assuming authority | ||
| was refunded. | ||
|
|
||
| ## Settle or quarantine | ||
|
|
||
| After a trusted adapter verifies provider usage, settle against the originally | ||
| bound quote: | ||
|
|
||
| ```python | ||
| result = proxy.settle_spend( | ||
| session, | ||
| request_id=request.request_id, | ||
| actual_input_tokens=3_700, | ||
| actual_output_tokens=1_250, | ||
| usage_proof_digest="a" * 64, | ||
| ) | ||
| ``` | ||
|
|
||
| The digest is a bounded reference to trusted usage evidence; raw provider | ||
| responses and usage documents do not enter the signed receipt. Settlement | ||
| recomputes cost using the quote selected at reservation. It consumes actual | ||
| usage and refunds only the verified remainder. | ||
|
|
||
| If trusted usage is missing, quarantine explicitly: | ||
|
|
||
| ```python | ||
| proxy.quarantine_spend( | ||
| session, | ||
| request_id=request.request_id, | ||
| reason_code="spend_settlement_evidence_missing", | ||
| ) | ||
| ``` | ||
|
|
||
| `quarantine_stale_spend()` moves old active reservations to quarantine without | ||
| refunding them. It scans only reservations owned by the supplied governance | ||
| session. A later trusted `settle_spend()` reconciles a quarantined reservation. | ||
| Usage beyond the reserved input/output bounds remains quarantined; a | ||
| post-action observation cannot retroactively authorize an overspend. | ||
|
|
||
| Every active reservation must be settled, released, or quarantined before | ||
| `end_session()` or attestation finalization. Once finalized, the proxy rejects | ||
| all spend lifecycle changes so the signed receipt chain cannot be extended | ||
| after its terminal summary. Reconcile quarantined usage before finalization if | ||
| the resulting settlement must appear in that session's signed evidence. | ||
|
|
||
| ## Evidence and metrics | ||
|
|
||
| Metered action receipts carry a spend-shaped `budget_delta` with operation, | ||
| requested/reserved/actual/refunded amounts, scope-level remaining authority, | ||
| currency, quote digest, reservation hash, and stable reason code. Settlement | ||
| and quarantine append separate signed receipt-chain links. | ||
|
|
||
| The runtime emits: | ||
|
|
||
| - `ardur_spend_events_total{operation,outcome}` | ||
| - `ardur_spend_amount_total{operation,unit}` | ||
|
|
||
| These labels never include agent, session, lineage, quote, model, request, or | ||
| account identifiers. | ||
|
|
||
| ## Failure modes and deployment boundary | ||
|
|
||
| - Missing, unknown, not-yet-valid, expired, mismatched, or unavailable quote | ||
| data fails closed. | ||
| - Missing trusted settlement retains the reservation. Conservative retention | ||
| can reduce availability and must be monitored. | ||
| - Exceptional evaluation compensates accepted reservations when possible. If | ||
| durable compensation cannot be proven, the reservation remains active and | ||
| session finalization fails closed. | ||
| - Closed records are retained through the passport lifetime for replay | ||
| protection. Expired terminal records may be pruned; settled amounts remain | ||
| archived in aggregate scope accounting. Active and quarantined reservations | ||
| are never age-refunded. | ||
| - The reference ledger coordinates processes that share one local state | ||
| directory. It is not a distributed consensus mechanism. Multi-host proxies | ||
| need a transactional `SpendBudgetLedger` implementation with equivalent | ||
| atomic and idempotent semantics. | ||
| - Quote configuration and trusted usage adapters are part of the operator's | ||
| trusted computing base. | ||
| - This surface does not reconcile cloud bills, implement chargeback, or prove | ||
| provider metering independently. | ||
|
|
||
| The design rationale is recorded in | ||
| [ADR-025](../decisions/ADR-025-pre-action-spend-reservation.md). |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Regenerate and commit the generated documentation.
The Hugo validation job currently fails because site/scripts/sync_source_docs.py --check reports a missing generated file. Run the repository’s source-doc synchronization step, commit the generated counterpart for this page, and rerun the check before merging.
🧰 Tools
🪛 GitHub Actions: hugo-site / 2_Validate and build Hugo site.txt
[error] 1-1: site/scripts/sync_source_docs.py --check: missing generated file.
🪛 LanguageTool
[grammar] ~200-~200: Ensure spelling is correct
Context: ...ator's trusted computing base. - This surface does not reconcile cloud bills, impleme...
(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)
[grammar] ~204-~204: Ensure spelling is correct
Context: ...design rationale is recorded in ADR-025.
(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_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 `@docs/reference/spend-budgets.md` around lines 1 - 204, Regenerate the
generated documentation counterpart for the page using the repository’s
source-doc synchronization step, ensuring the output reflects the current
spend-budget content. Verify the result with site/scripts/sync_source_docs.py
--check and commit the generated file.
Source: Pipeline failures
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (3)
docs/reference/README.md (1)
25-27: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd spend-budget sources to the page-maintenance list.
The new
spend-budgets.mdpage is listed, but the “When To Update These Pages” section does not mention its implementation sources, such aspython/vibap/spend_budget.py,python/vibap/proxy.py, andpython/vibap/metrics.py. Add those sources or explicitly document the generation relationship to prevent this reference page from drifting.🤖 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 `@docs/reference/README.md` around lines 25 - 27, Add the spend-budget implementation sources to the “When To Update These Pages” maintenance guidance for the Pre-action Spend Budgets entry, including python/vibap/spend_budget.py, python/vibap/proxy.py, and python/vibap/metrics.py, or document the generation relationship that keeps spend-budgets.md synchronized with them.python/vibap/spend_budget.py (1)
465-473: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueUndocumented but load-bearing: per-process
RLockguards a realflock()gotcha.
flock()locks are tied to the open file description, not the process; two threads that each independentlyopen()the lock file (as_lockeddoes on every call) won't actually exclude each other viaflockalone. The_LOCKS/process_lock.locklayer is what makes same-process concurrent calls safe. This is correct, but a future refactor could easily "simplify" away the seemingly-redundantRLockwithout realizing it reintroduces an intra-process race. A short comment explaining why both locks are needed would help.Also applies to: 1283-1302
🤖 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 `@python/vibap/spend_budget.py` around lines 465 - 473, The per-process _LOCKS RLock layer in _ProcessLock and its use in _locked must remain documented as necessary alongside flock(); add a concise comment explaining that independently opened lock files share no flock exclusion within the process, while the RLock serializes same-process callers. Preserve both locking layers and place the explanation near the lock definitions or _locked implementation.python/vibap/proxy.py (1)
3771-3965: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueLifecycle methods share the same
mission_ref/spend_budgetresolution as noted above.
settle_spend,quarantine_spend, andquarantine_stale_spendall resolvespend_budgetvia_resolve_authoritative_policy_claims, so they inherit the same potential gap flagged at_reserve_spend_if_required(Lines 2339-2483) formission_refsessions. Separately,settle_spendexplicitly guardsraw_policy is Nonewith a dedicatedspend_policy_missingreason (Lines 3794-3797), whilequarantine_spend/quarantine_stale_spendpasspolicy_claims.get("spend_budget")straight intonormalize_spend_budget, relying on its genericspend_policy_invalidmessage instead. Low-impact, but worth aligning for consistent operator-facing reason codes.🤖 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 `@python/vibap/proxy.py` around lines 3771 - 3965, Align spend lifecycle policy resolution across settle_spend, quarantine_spend, and quarantine_stale_spend with the mission_ref handling used by _reserve_spend_if_required, ensuring all methods resolve the authoritative spend_budget consistently. Also add the same explicit missing-policy guard and spend_policy_missing reason used by settle_spend before normalize_spend_budget in quarantine_spend and quarantine_stale_spend, while preserving existing invalid-policy handling.
🤖 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 `@python/vibap/proxy.py`:
- Around line 2339-2483: Update MissionDeclaration.policy_claims() to propagate
the passport’s spend_budget claim so _reserve_spend_if_required() enforces spend
caps for mission_ref sessions. Preserve the existing policy normalization and
metered-tool checks; only exclude mission_ref explicitly if that behavior is
intentional and documented.
In `@site/content/source/docs/reference/spend-budgets.md`:
- Line 3: Complete the frontmatter description value so the sentence beginning
“Ardur's Python library can reserve token and monetary authority before a” is
grammatically finished and accurately summarizes the spend-budgets page.
---
Nitpick comments:
In `@docs/reference/README.md`:
- Around line 25-27: Add the spend-budget implementation sources to the “When To
Update These Pages” maintenance guidance for the Pre-action Spend Budgets entry,
including python/vibap/spend_budget.py, python/vibap/proxy.py, and
python/vibap/metrics.py, or document the generation relationship that keeps
spend-budgets.md synchronized with them.
In `@python/vibap/proxy.py`:
- Around line 3771-3965: Align spend lifecycle policy resolution across
settle_spend, quarantine_spend, and quarantine_stale_spend with the mission_ref
handling used by _reserve_spend_if_required, ensuring all methods resolve the
authoritative spend_budget consistently. Also add the same explicit
missing-policy guard and spend_policy_missing reason used by settle_spend before
normalize_spend_budget in quarantine_spend and quarantine_stale_spend, while
preserving existing invalid-policy handling.
In `@python/vibap/spend_budget.py`:
- Around line 465-473: The per-process _LOCKS RLock layer in _ProcessLock and
its use in _locked must remain documented as necessary alongside flock(); add a
concise comment explaining that independently opened lock files share no flock
exclusion within the process, while the RLock serializes same-process callers.
Preserve both locking layers and place the explanation near the lock definitions
or _locked implementation.
🪄 Autofix (Beta)
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: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 8b230337-2e38-4517-adef-cece274cbd67
📒 Files selected for processing (31)
README.mddocs/decisions/ADR-025-pre-action-spend-reservation.mddocs/decisions/README.mddocs/reference/README.mddocs/reference/spend-budgets.mddocs/specs/ardur-drp-mapping-v0.1.jsondocs/specs/ardur-drp-mapping-v0.1.mddocs/specs/execution-receipt-v0.2.schema.jsonpython/README.mdpython/tests/test_spend_budget.pypython/vibap/__init__.pypython/vibap/_specs/execution_receipt_v02.schema.jsonpython/vibap/metrics.pypython/vibap/passport.pypython/vibap/proxy.pypython/vibap/receipt.pypython/vibap/spend_budget.pysite/content/source/README.mdsite/content/source/_index.mdsite/content/source/docs/decisions/ADR-025-pre-action-spend-reservation.mdsite/content/source/docs/decisions/README.mdsite/content/source/docs/decisions/_index.mdsite/content/source/docs/reference/README.mdsite/content/source/docs/reference/_index.mdsite/content/source/docs/reference/spend-budgets.mdsite/content/source/docs/specs/ardur-drp-mapping-v0.1.mdsite/content/source/python/README.mdsite/data/source_routes.jsonsite/static/repo/docs/specs/ardur-drp-mapping-v0.1.jsonsite/static/repo/docs/specs/execution-receipt-v0.2.schema.jsonsite/static/repo/python/vibap/_specs/execution_receipt_v02.schema.json
| @staticmethod | ||
| def _zero_spend_amounts() -> dict[str, int]: | ||
| return {"tokens": 0, "currency_micros": 0} | ||
|
|
||
| @classmethod | ||
| def _spend_reservation_delta( | ||
| cls, | ||
| result: SpendReservationResult, | ||
| ) -> dict[str, Any]: | ||
| zero = cls._zero_spend_amounts() | ||
| return { | ||
| "operation": "reserve" if result.accepted else "reject", | ||
| "resource": "spend", | ||
| "requested": dict(result.reserved), | ||
| "reserved": dict(result.reserved) if result.accepted else zero, | ||
| "actual": cls._zero_spend_amounts(), | ||
| "refunded": cls._zero_spend_amounts(), | ||
| "remaining": copy.deepcopy(result.remaining), | ||
| "currency": result.currency, | ||
| "quote_digest": result.quote_digest, | ||
| "reservation_hash": result.reservation_hash, | ||
| "reason_code": result.reason_code, | ||
| "idempotent": result.idempotent, | ||
| } | ||
|
|
||
| @classmethod | ||
| def _spend_close_delta(cls, result: SpendCloseResult) -> dict[str, Any]: | ||
| retained = ( | ||
| dict(result.reserved) | ||
| if result.operation == "quarantine" | ||
| else cls._zero_spend_amounts() | ||
| ) | ||
| return { | ||
| "operation": result.operation, | ||
| "resource": "spend", | ||
| "requested": dict(result.reserved), | ||
| "reserved": retained, | ||
| "actual": dict(result.actual), | ||
| "refunded": dict(result.refunded), | ||
| "remaining": copy.deepcopy(result.remaining), | ||
| "currency": result.currency, | ||
| "quote_digest": result.quote_digest, | ||
| "reservation_hash": result.reservation_hash, | ||
| "reason_code": result.reason_code, | ||
| "idempotent": result.idempotent, | ||
| "reconciled": result.reconciled, | ||
| } | ||
|
|
||
| @staticmethod | ||
| def _spend_budget_remaining(delta: Mapping[str, Any]) -> dict[str, int]: | ||
| remaining: dict[str, int] = {} | ||
| raw_remaining = delta.get("remaining") | ||
| currency = str(delta.get("currency", "")).lower() | ||
| if not isinstance(raw_remaining, Mapping) or not currency: | ||
| return remaining | ||
| for scope in ("session", "agent", "lineage"): | ||
| amounts = raw_remaining.get(scope) | ||
| if not isinstance(amounts, Mapping): | ||
| continue | ||
| for dimension, bucket in ( | ||
| ("tokens", "tokens"), | ||
| ("currency_micros", f"{currency}_micros"), | ||
| ): | ||
| raw_amount = amounts.get(dimension) | ||
| if isinstance(raw_amount, int) and not isinstance(raw_amount, bool): | ||
| remaining[f"spend.{scope}.{bucket}"] = max(0, raw_amount) | ||
| return remaining | ||
|
|
||
| def _reserve_spend_if_required( | ||
| self, | ||
| session: GovernanceSession, | ||
| tool_name: str, | ||
| policy_claims: Mapping[str, Any], | ||
| spend_request: SpendReservationRequest | None, | ||
| ) -> SpendReservationResult | None: | ||
| raw_policy = policy_claims.get("spend_budget") | ||
| if raw_policy is None: | ||
| if spend_request is not None: | ||
| raise SpendBudgetError("spend_request_unexpected") | ||
| return None | ||
| policy = normalize_spend_budget(raw_policy, require_lineage_id=True) | ||
| if tool_name not in policy["metered_tools"]: | ||
| if spend_request is not None: | ||
| raise SpendBudgetError("spend_request_unexpected") | ||
| return None | ||
| if spend_request is None: | ||
| raise SpendBudgetError("spend_reservation_missing") | ||
| if not isinstance(spend_request, SpendReservationRequest): | ||
| raise SpendBudgetError("spend_request_invalid") | ||
| if self.spend_quote_store is None: | ||
| raise SpendBudgetError("spend_quote_store_unavailable") | ||
| quote = self.spend_quote_store.resolve( | ||
| spend_request.quote_id, | ||
| tool_name=tool_name, | ||
| model=spend_request.model, | ||
| currency=str(policy["currency"]), | ||
| ) | ||
| return self.spend_budget_ledger.reserve( | ||
| policy=policy, | ||
| session_id=session.jti, | ||
| agent_id=str(session.passport_claims.get("sub", "unknown")), | ||
| request=spend_request, | ||
| quote=quote, | ||
| retention_until=int(session.passport_claims["exp"]), | ||
| ) | ||
|
|
||
| @staticmethod | ||
| def _record_spend_amount_metrics(operation: str, amounts: Mapping[str, int]) -> None: | ||
| for unit in ("tokens", "currency_micros"): | ||
| amount = amounts.get(unit) | ||
| if isinstance(amount, int) and not isinstance(amount, bool): | ||
| ardur_metrics.spend_amount_total.add( | ||
| max(0, amount), | ||
| operation=operation, | ||
| unit=unit, | ||
| ) | ||
|
|
||
| @classmethod | ||
| def _record_spend_reservation_metrics( | ||
| cls, | ||
| result: SpendReservationResult, | ||
| ) -> None: | ||
| operation = "reserve" if result.accepted else "reject" | ||
| ardur_metrics.spend_events_total.inc( | ||
| operation=operation, | ||
| outcome=result.reason_code, | ||
| ) | ||
| cls._record_spend_amount_metrics(operation, result.reserved) | ||
|
|
||
| @classmethod | ||
| def _record_spend_close_metrics(cls, result: SpendCloseResult) -> None: | ||
| ardur_metrics.spend_events_total.inc( | ||
| operation=result.operation, | ||
| outcome=result.reason_code, | ||
| ) | ||
| if result.operation == "settle": | ||
| cls._record_spend_amount_metrics("settle", result.actual) | ||
| cls._record_spend_amount_metrics("refund", result.refunded) | ||
| if result.reconciled: | ||
| cls._record_spend_amount_metrics("reconcile", result.actual) | ||
| elif result.operation == "release": | ||
| cls._record_spend_amount_metrics("release", result.refunded) | ||
| elif result.operation == "quarantine": | ||
| cls._record_spend_amount_metrics("quarantine", result.reserved) | ||
|
|
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
rg -n -A5 "def policy_claims" python/vibap/mission.py
rg -n "spend_budget" python/vibap/mission.py python/vibap/passport.py
rg -n -B2 -A20 "mission_ref" python/vibap/passport.pyRepository: ArdurAI/ardur
Length of output: 2057
mission_ref sessions drop spend_budget, so spend-cap enforcement is skipped.
MissionDeclaration.policy_claims() does not propagate spend_budget, which makes _reserve_spend_if_required() treat those calls as unmetered even when the passport carries a spend policy. Either carry that claim through or explicitly document that mission_ref is excluded from spend budgets.
🤖 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 `@python/vibap/proxy.py` around lines 2339 - 2483, Update
MissionDeclaration.policy_claims() to propagate the passport’s spend_budget
claim so _reserve_spend_if_required() enforces spend caps for mission_ref
sessions. Preserve the existing policy normalization and metered-tool checks;
only exclude mission_ref explicitly if that behavior is intentional and
documented.
| @@ -0,0 +1,221 @@ | |||
| --- | |||
| title: "Pre-action spend budgets" | |||
| description: "Ardur's Python library can reserve token and monetary authority before a" | |||
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Complete the page description.
The frontmatter description is truncated at “before a”, so generated metadata will expose an incomplete sentence.
Proposed fix
-description: "Ardur's Python library can reserve token and monetary authority before a"
+description: "Ardur's Python library can reserve token and monetary authority before a metered tool or provider call."📝 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.
| description: "Ardur's Python library can reserve token and monetary authority before a" | |
| description: "Ardur's Python library can reserve token and monetary authority before a metered tool or provider call." |
🤖 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 `@site/content/source/docs/reference/spend-budgets.md` at line 3, Complete the
frontmatter description value so the sentence beginning “Ardur's Python library
can reserve token and monetary authority before a” is grammatically finished and
accurately summarizes the spend-budgets page.
Clears the CONFLICTING state after #303, #320 and #322 landed on dev. proxy.py conflicted almost entirely as formatting noise: #320 ruff-formatted the file, so the merge base was unformatted while both sides were formatted. Normalising all three sides with ruff format before the three-way merge reduced it to one region -- the import block, where #320 had relocated the metrics, rate_limiter and tls imports, the _SessionCoordinationLock block and the module constants, while this branch added spend_budget beside them. Resolved by keeping only spend_budget, after verifying every other element of the region already exists elsewhere in the merged file. Verified no control was dropped, counting ours/dev/merged occurrences: approval_fatigue_threshold 2/2/2 and _SessionCoordinationLock 5/5/5, TLSConfigurationError 0/5/5 (gained from dev), spend_budget 29/0/29, normalize_spend_budget 8/0/8, SpendReservationRequest 5/0/5. The docs/reference index conflict was additive on both sides and resolved as a union; Hugo mirrors were regenerated rather than hand-picked. Checkpoint: architect/sessions/issue-310-spend-gates/2026-07-14-journal.md Signed-off-by: Gnani Rahul Nutakki <gnani.nutakki@gmail.com>
|
Not merging this one in the current integration pass — leaving it to you rather than guessing. #316 (risk budgets) landed on The bulk of the Full write-up, including two latent traps a naive merge would ship ( |
Relevant issues
Closes #310.
Part of #170.
Type
feat— new capability or surfacefix— bug fixdocs— article, README, or doc changerefactor— internal change, no behaviour changetest— test-only changeci— CI / workflow / toolingchore— housekeepingdev → main graduation— promoting reviewed work to release branchChanges
PERMIT, deny cap breaches before executor invocation, and compensate accepted reservations when evaluation fails before returningThe local CodeRabbit first pass produced eight findings; each was independently traced and corrected. Two follow-up local reviews did not complete within bounded waits, so this PR does not claim a clean second-pass review and remains subject to GitHub review and CI.
Testing
uv run --project python --locked --extra dev python -m pytest -q python/tests/test_spend_budget.py python/tests/test_passport.py python/tests/test_proxy.py python/tests/test_receipt_schema_v02.py(115 passed)Full provider end-to-end testing was intentionally not run for this local reference slice; targeted pre-execution, concurrency, restart, replay, settlement, privacy, lifecycle, and packaging tests cover the changed boundaries. CI remains authoritative for the repository-wide matrix.
Graduation gates (for
dev → mainPRs only)Not applicable. This PR targets
dev; human review remains mandatory, anddevtomainpromotion is out of scope.Summary by CodeRabbit