Skip to content

feat(governance): enforce pre-action spend caps#314

Open
gnanirahulnutakki wants to merge 3 commits into
devfrom
gnanirahulnutakki/issue-310-spend-gates
Open

feat(governance): enforce pre-action spend caps#314
gnanirahulnutakki wants to merge 3 commits into
devfrom
gnanirahulnutakki/issue-310-spend-gates

Conversation

@gnanirahulnutakki

@gnanirahulnutakki gnanirahulnutakki commented Jul 14, 2026

Copy link
Copy Markdown
Member

Relevant issues

Closes #310.

Part of #170.

Type

  • feat — new capability or surface
  • fix — bug fix
  • docs — article, README, or doc change
  • refactor — internal change, no behaviour change
  • test — test-only change
  • ci — CI / workflow / tooling
  • chore — housekeeping
  • dev → main graduation — promoting reviewed work to release branch

Changes

  • add an optional signed spend policy with distinct integer token and currency-micro ceilings for session, agent, and delegation-lineage scopes
  • bind roots to a fresh spend lineage and make derived children inherit the exact authority
  • add immutable operator quote snapshots and a file-locked reserve/settle/quarantine ledger with atomic replacement, replay handling, terminal retention, and conservative recovery
  • reserve before PERMIT, deny cap breaches before executor invocation, and compensate accepted reservations when evaluation fails before returning
  • bind every close/reconcile/stale operation to its owning session so siblings cannot mutate foreign reservations or receipt chains
  • block finalization while reservations are active and reject spend lifecycle changes after finalization
  • extend signed execution receipts and offline verification with bounded spend deltas; exclude prompts, raw responses, request IDs, account identifiers, credentials, raw quotes, and host paths
  • expose bounded lifecycle/amount metrics without high-cardinality identity labels
  • document the trust, durability, availability, distributed-deployment, and cost boundaries in ADR-025 and the spend-budget reference

The 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)
  • CI-pinned Ruff 0.13.0 check on every touched Python file, with only the proxy's pre-existing E402 import-layout exception isolated (passed)
  • Ruff format check on both new Python files (passed)
  • exact-file pre-commit hygiene/security hooks: whitespace, EOF, JSON, size, private-key detection, line endings, and gitleaks (passed)
  • canonical and embedded execution-receipt schema parity (passed)
  • sdist and wheel build; verified the wheel contains the spend ledger module and embedded execution-receipt schema
  • signed DCO commit and clean worktree verified

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 → main PRs only)

Not applicable. This PR targets dev; human review remains mandatory, and dev to main promotion is out of scope.

Summary by CodeRabbit

  • New Features
    • Added pre-action spend budgets for metered tool calls, including reservation, conservative settlement, and quarantine of uncertain usage.
    • Extended passport authority to carry optional spend-budget claims and supported spend reservation/closure lifecycle in the governance proxy.
    • Added spend-specific receipt details and remaining-budget reporting, plus new bounded spend metrics.
  • Documentation
    • Added and indexed ADR-025 and published the “Pre-action Spend Budgets” reference page, including mapping and schema updates.
  • Tests
    • Added comprehensive spend-budget validation, reservation/settlement/quarantine, concurrency, idempotency, and fail-closed scenarios.

Signed-off-by: Gnani Rahul Nutakki <gnani.nutakki@gmail.com>
@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 1572881e-0377-4fb4-9e4f-8479e1eeeb6b

📥 Commits

Reviewing files that changed from the base of the PR and between ccfd67f and a44e148.

📒 Files selected for processing (10)
  • README.md
  • docs/decisions/README.md
  • docs/reference/README.md
  • python/vibap/proxy.py
  • site/content/source/README.md
  • site/content/source/_index.md
  • site/content/source/docs/decisions/README.md
  • site/content/source/docs/reference/README.md
  • site/content/source/docs/reference/_index.md
  • site/data/source_routes.json
🚧 Files skipped from review as they are similar to previous changes (9)
  • site/content/source/_index.md
  • site/content/source/docs/reference/_index.md
  • site/content/source/docs/decisions/README.md
  • docs/reference/README.md
  • site/data/source_routes.json
  • site/content/source/README.md
  • README.md
  • docs/decisions/README.md
  • python/vibap/proxy.py

📝 Walkthrough

Walkthrough

Adds 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.

Changes

Spend budget governance

Layer / File(s) Summary
Policy, quotes, and durable ledger
python/vibap/spend_budget.py
Defines spend-policy normalization, quote snapshots, reservation/closure contracts, and a file-backed locked ledger supporting reservation, settlement, cancellation, quarantine, reconciliation, idempotency, and integrity validation.
Passport and receipt contracts
python/vibap/passport.py, python/vibap/receipt.py, python/vibap/_specs/..., docs/specs/..., site/static/repo/..., python/vibap/__init__.py
Adds lineage-bound spend_budget passport claims, spend receipt validation and schemas, DRP mappings, and public package exports.
Proxy enforcement and lifecycle
python/vibap/proxy.py, python/vibap/metrics.py
Integrates pre-execution reservation, permit/deny handling, compensation, settlement, quarantine, finalization gating, spend receipt deltas, and bounded Prometheus metrics.
End-to-end validation
python/tests/test_spend_budget.py
Covers policy and quote validation, concurrency, duplicate handling, settlement and quarantine recovery, lineage enforcement, proxy behavior, compensation failures, finalization, metrics, and non-metered compatibility.
Architecture and reference documentation
README.md, docs/decisions/..., docs/reference/..., python/README.md, site/content/source/..., site/data/source_routes.json
Documents ADR-025, spend-budget configuration and lifecycle semantics, deployment boundaries, reference indexes, generated mirrors, and source routes.

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
Loading

Possibly related PRs

  • ArdurAI/ardur#316: Modifies overlapping passport and proxy governance surfaces for authenticated, ledger-backed runtime controls.
  • ArdurAI/ardur#318: Adds governed child execution hooks that connect to the spend-budget pathways introduced here.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 4.03% 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main governance change: enforcing pre-action spend caps.
Linked Issues check ✅ Passed The PR implements pre-action spend caps, immutable quotes, reserve/settle/quarantine flows, idempotency, lineage binding, and bounded receipts/metrics required by #310.
Out of Scope Changes check ✅ Passed The changes stay focused on the spend-cap feature and its supporting docs, schemas, tests, and mirrors without obvious unrelated additions.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch gnanirahulnutakki/issue-310-spend-gates

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

Comment thread python/tests/test_spend_budget.py Fixed
Comment thread python/tests/test_spend_budget.py Fixed
Signed-off-by: Gnani Rahul Nutakki <gnani.nutakki@gmail.com>
@gnanirahulnutakki

Copy link
Copy Markdown
Member Author

CI repair pushed in signed commit ccfd67f.

Root cause and repair:

  • Python 3.10/3.13: added the missing spend_budget authority entry to the canonical DRP mapping inventory. The mapping reserves metadata.x-ardur.budget.spendBudget while explicitly preserving the current fail-closed non-projection boundary.
  • Hugo: regenerated and committed the source-backed documentation and schema mirrors.
  • CodeQL review comments: removed the two duplicate module imports by using fully qualified monkeypatch targets.

Targeted local verification:

  • 118 passed: spend budget, passport, proxy, receipt schema, and DRP mapping contract suites
  • source-doc sync: 117 pages and 113 artifacts verified
  • public claims: 10 validated
  • exact changed-file pre-commit: all applicable hooks passed
  • canonical and embedded receipt schemas: byte-identical

Fresh CI is running against ccfd67f.

@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: 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

📥 Commits

Reviewing files that changed from the base of the PR and between eb63d22 and 598c53e.

📒 Files selected for processing (15)
  • README.md
  • docs/decisions/ADR-025-pre-action-spend-reservation.md
  • docs/decisions/README.md
  • docs/reference/README.md
  • docs/reference/spend-budgets.md
  • docs/specs/execution-receipt-v0.2.schema.json
  • python/README.md
  • python/tests/test_spend_budget.py
  • python/vibap/__init__.py
  • python/vibap/_specs/execution_receipt_v02.schema.json
  • python/vibap/metrics.py
  • python/vibap/passport.py
  • python/vibap/proxy.py
  • python/vibap/receipt.py
  • python/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

Comment on lines +1 to +204
# 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).

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ 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

@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: 2

🧹 Nitpick comments (3)
docs/reference/README.md (1)

25-27: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add spend-budget sources to the page-maintenance list.

The new spend-budgets.md page is listed, but the “When To Update These Pages” section does not mention its implementation sources, such as python/vibap/spend_budget.py, python/vibap/proxy.py, and python/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 value

Undocumented but load-bearing: per-process RLock guards a real flock() gotcha.

flock() locks are tied to the open file description, not the process; two threads that each independently open() the lock file (as _locked does on every call) won't actually exclude each other via flock alone. The _LOCKS/process_lock.lock layer is what makes same-process concurrent calls safe. This is correct, but a future refactor could easily "simplify" away the seemingly-redundant RLock without 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 value

Lifecycle methods share the same mission_ref/spend_budget resolution as noted above.

settle_spend, quarantine_spend, and quarantine_stale_spend all resolve spend_budget via _resolve_authoritative_policy_claims, so they inherit the same potential gap flagged at _reserve_spend_if_required (Lines 2339-2483) for mission_ref sessions. Separately, settle_spend explicitly guards raw_policy is None with a dedicated spend_policy_missing reason (Lines 3794-3797), while quarantine_spend/quarantine_stale_spend pass policy_claims.get("spend_budget") straight into normalize_spend_budget, relying on its generic spend_policy_invalid message 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

📥 Commits

Reviewing files that changed from the base of the PR and between eb63d22 and ccfd67f.

📒 Files selected for processing (31)
  • README.md
  • docs/decisions/ADR-025-pre-action-spend-reservation.md
  • docs/decisions/README.md
  • docs/reference/README.md
  • docs/reference/spend-budgets.md
  • docs/specs/ardur-drp-mapping-v0.1.json
  • docs/specs/ardur-drp-mapping-v0.1.md
  • docs/specs/execution-receipt-v0.2.schema.json
  • python/README.md
  • python/tests/test_spend_budget.py
  • python/vibap/__init__.py
  • python/vibap/_specs/execution_receipt_v02.schema.json
  • python/vibap/metrics.py
  • python/vibap/passport.py
  • python/vibap/proxy.py
  • python/vibap/receipt.py
  • python/vibap/spend_budget.py
  • site/content/source/README.md
  • site/content/source/_index.md
  • site/content/source/docs/decisions/ADR-025-pre-action-spend-reservation.md
  • site/content/source/docs/decisions/README.md
  • site/content/source/docs/decisions/_index.md
  • site/content/source/docs/reference/README.md
  • site/content/source/docs/reference/_index.md
  • site/content/source/docs/reference/spend-budgets.md
  • site/content/source/docs/specs/ardur-drp-mapping-v0.1.md
  • site/content/source/python/README.md
  • site/data/source_routes.json
  • site/static/repo/docs/specs/ardur-drp-mapping-v0.1.json
  • site/static/repo/docs/specs/execution-receipt-v0.2.schema.json
  • site/static/repo/python/vibap/_specs/execution_receipt_v02.schema.json

Comment thread python/vibap/proxy.py
Comment on lines +2339 to +2483
@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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 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.py

Repository: 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"

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

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.

Suggested change
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>
@gnanirahulnutakki

Copy link
Copy Markdown
Member Author

Not merging this one in the current integration pass — leaving it to you rather than guessing.

#316 (risk budgets) landed on dev first, so this branch is now CONFLICTING. It is left at its pushed head a44e148 (which was green); nothing was force-pushed and no bad merge was published.

The bulk of the proxy.py conflict is formatting noise from #320's ruff-format pass and resolves mechanically. What does not is the nested dispatch control flow: this PR's approval-fatigue / _spend_compensation block and #316's risk preflight/release block are different parts of the same restructured if/else, and composing them is a design decision — gate ordering, and what happens when one gate permits while the other denies.

Full write-up, including two latent traps a naive merge would ship (_receipt_budget_remaining changed arity from 2 to 3 args, and the import/constructor hunks split across parentheses), is in #325.

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.

[D][Governance] Enforce pre-action monetary and token spend caps

2 participants