You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
- float → Decimal throughout: BudgetLimit.amount, BudgetWindow, store, tests
- BudgetLimit + BudgetWindow model: config refactored from flat fields to
limits: list[BudgetLimit]; each limit has amount, currency, scope_by, window
- Atomic check_and_record(): eliminates TOCTOU race on get_spend()+record_spend();
InMemorySpendStore implements with threading.Lock (single-process); docs note
production stores need DB-level atomics (Postgres FOR UPDATE, Redis Lua)
- scope_by field: independent per-dimension budget isolation; scope_by=(channel,)
means channel A spend does not count against channel B budget
- selector.path fix: config examples and README updated to use 'input' not '*';
Step vs raw-dict distinction documented; evaluator auto-detects format
- EvaluatorResult.error usage: malformed payload returns matched=False, error=None;
error field reserved for crashes/timeouts/missing deps only
- README: custom store example updated with scope param and check_and_record;
stale malformed-input docs corrected; Known Limitations updated
- Tests: or True removed; all assertions verify actual store state; lan17
channel isolation test (90 in A + 20 in B) passes with scope_by semantics
@@ -10,10 +10,12 @@ As agents transact autonomously via protocols like [x402](https://github.com/coi
10
10
11
11
Tracks cumulative agent spend and enforces rolling budget limits. Stateful — records approved transactions and checks new ones against accumulated spend.
12
12
13
-
-**Per-transaction cap** — reject any single payment above a threshold
14
-
-**Rolling period budget** — reject payments that would exceed a time-windowed budget
15
-
-**Context-aware overrides** — different limits per channel, agent, or session via evaluate metadata
13
+
-**Per-transaction cap** — reject any single payment above a threshold (`BudgetLimit` with no window)
14
+
-**Rolling period budget** — reject payments that would exceed a time-windowed budget (`BudgetWindow(kind="rolling", ...)`)
15
+
-**Calendar-aligned budget** — reject payments that exceed a day/week/month budget (`BudgetWindow(kind="fixed", ...)`)
16
+
-**Scoped budgets** — independent counters per channel, agent, or session via `scope_by`
16
17
-**Pluggable storage** — abstract `SpendStore` protocol with built-in `InMemorySpendStore`; bring your own PostgreSQL, Redis, etc.
18
+
-**Atomic enforcement** — `check_and_record()` prevents TOCTOU races in single-process deployments
17
19
18
20
### `financial_governance.transaction_policy`
19
21
@@ -35,16 +37,25 @@ pip install -e ".[dev]"
35
37
36
38
### Spend Limit
37
39
40
+
The `spend_limit` evaluator is configured via a list of `BudgetLimit` objects. Each limit is evaluated independently — the first violation wins.
41
+
38
42
```yaml
39
43
controls:
40
44
- name: spend-limit
41
45
evaluator:
42
46
type: financial_governance.spend_limit
43
47
config:
44
-
max_per_transaction: 100.0# Max USDC per single payment
> **Note:** Use `Decimal` or string representations for `amount` — never raw `float`. Floating-point arithmetic is imprecise for money. The evaluator internally converts to `Decimal`.
107
+
108
+
## BudgetLimit Model
109
+
110
+
```python
111
+
from decimal import Decimal
112
+
from agent_control_evaluator_financial_governance.spend_limit import (
When using `selector.path: "*"`, the evaluator merges `step.context` fields into the transaction data automatically. When using `selector.path: "input"`, context fields must be included directly in `step.input`.
174
+
When using `selector.path: "*"`, the evaluator merges `step.context` fields into the transaction data automatically. Fields already present in `step.input` are never overwritten by context.
112
175
113
176
**Option B: Inline in the transaction dict** (simpler, for direct SDK use)
114
177
115
178
```python
116
179
result = await evaluator.evaluate({
117
-
"amount": 75.0,
180
+
"amount": "75.00",
118
181
"currency": "USDC",
119
182
"recipient": "0xABC",
120
183
"channel": "experimental",
121
-
"channel_max_per_transaction": 50.0,
122
-
"channel_max_per_period": 200.0,
184
+
"agent_id": "agent-42",
123
185
})
124
186
```
125
187
126
-
Spend budgets are **scoped by context** — spend in channel A does not count against channel B's budget. When no context fields are present, budgets are global.
127
-
128
188
## Custom SpendStore
129
189
130
-
The `SpendStore` protocol requires two methods. Implement them for your backend:
190
+
The `SpendStore` protocol requires three methods. Implement them for your backend:
131
191
132
192
```python
193
+
from decimal import Decimal
133
194
from agent_control_evaluator_financial_governance.spend_limit import (
> **Single-process atomicity note:** `InMemorySpendStore.check_and_record()` uses a `threading.Lock` to atomically check-and-record within a single process. For multi-process or distributed deployments, your custom store must implement true database-level atomics (e.g., PostgreSQL `SELECT ... FOR UPDATE`, Redis Lua scripts).
267
+
163
268
## Running Tests
164
269
165
270
```bash
@@ -170,10 +275,12 @@ pytest tests/ -v
170
275
171
276
## Design Decisions
172
277
173
-
1. **Decoupled from data source** — The `SpendStore` protocol means no new tables in core Agent Control. Bring your own persistence.
174
-
2. **Context-aware limits** — Override keys in the evaluate data dict allow per-channel, per-agent, or per-session limits without multiple evaluator instances.
175
-
3. **Python SDK compatible** — Uses the standard evaluator interface; works with both the server and the Python SDK evaluation engine.
176
-
4. **Fail-open on errors** — Missing or malformed data returns `matched=False` with an `error` field, following Agent Control conventions.
278
+
1. **Decimal for money** — All monetary amounts use `Decimal`, never `float`. Floating-point arithmetic is unsuitable for financial calculations.
279
+
2. **BudgetLimit + BudgetWindow models** — Expressive, composable budget definitions that replace the previous flat config. Each limit is independent; first violation wins.
280
+
3. **Independent scope dimensions** — `scope_by=("channel",)` creates a separate counter for each channel value. Spend in one channel is completely isolated from another.
281
+
4. **Atomic check_and_record()** — Eliminates the TOCTOU race of separate `get_spend()` + `record_spend()` calls. Single-process safe with `threading.Lock`; production stores should use DB-level atomics.
282
+
5. **Decoupled from data source** — The `SpendStore` protocol means no new tables in core Agent Control. Bring your own persistence.
283
+
6. **Fail-open on malformed input** — Missing or malformed data returns `matched=False, error=None`, following Agent Control conventions. The `error` field is reserved for evaluator crashes, not policy decisions.
0 commit comments