Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,7 @@ Concretely — these are the design principles the repo is being built to meet,
- **Composable with what already exists.** Designed around SPIFFE for workload identity, Biscuit for first-party-attenuation credentials, Cedar for policy, the individual AAT Internet-Draft for delegation-token semantics, and EAT (RFC 9711) for attestation-token semantics. We didn't reinvent the substrate.
- **Cryptographically bound by design.** Mission credentials are designed to be signed by an issuer key and produce signed receipts chain-hashed to the previous one. The Python Biscuit path reports SPIFFE holder binding only when the proxy has a server-owned Biscuit issuer key, JWT-SVID trust bundle, and audience and the presented credentials verify against them; request payloads cannot choose those verifier inputs. JWT-SVID itself remains a replayable bearer credential, so this is bounded holder evidence rather than universal replay prevention. The design is documented in the [ADRs](docs/decisions/README.md); the public code that implements it is being curated in phases.
- **Delegation that narrows, never widens.** Child sessions get strictly narrower authority than their parent — fewer tools, smaller resource scope, smaller budget. The narrowing discipline is formalised in [ADR-017](docs/decisions/ADR-017-biscuit-attenuation-narrowing-semantics.md).
- **Pre-action spend authority.** Library adapters can reserve signed token and currency-micro ceilings across session, agent, and delegation-lineage scopes before a metered call, then settle trusted usage or quarantine uncertainty without silently refunding it. [Reference](docs/reference/spend-budgets.md).
- **No authority by omission.** An absent or empty `resource_scope` grants no resource authority. Operators who intentionally permit every resource must sign the sole explicit wildcard `resource_scope: ["**"]`; issuance and governed-run surfaces warn when they do. The decision and format-specific attenuation rules are documented in [ADR-023](docs/decisions/ADR-023-explicit-resource-scope-authority.md).
- **Explicit about what it doesn't do.** Scope-level governance can't catch semantic misuse — if an allowed tool is used on an allowed resource for the wrong reason, that's a different layer's job.
- **MIT licensed.** The research foundation (the Silence Theorem, the protocol formalism, the benchmark methodology) will be linked from this repo when the paper's public identifier is assigned. Articles in this repo paraphrase the research in original prose; they do not reproduce paper content.
Expand Down
107 changes: 107 additions & 0 deletions docs/decisions/ADR-025-pre-action-spend-reservation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# ADR-025: Pre-action spend reservation and conservative settlement

**Status:** Accepted

**Date:** 2026-07-14

## Context

Ardur already enforces governed tool-call counts and reserves descendant call
authority. A provider or metered tool call has a different budget lifecycle:
the final output quantity is unknown before execution, price depends on an
operator-approved quote, and actual usage arrives only after the action. A
post-call cost field cannot prevent an over-budget call.

Mutable provider prices, provider-returned model labels, and caller-supplied
rates are not authorization authority. Fetching current prices during policy
evaluation would also add network availability and latency to a fail-closed hot
path. Binary floating-point arithmetic is unsuitable for exact budget gates.

The existing local lineage ledger establishes Ardur's reference durability
model: process-local coordination plus an exclusive file lock, a temporary
state file, and atomic replacement. Python documents `flock(LOCK_EX)` as the
exclusive advisory-lock primitive and `os.replace()` as atomic on POSIX when
successful. RFC 8785 defines the invariant JSON representation Ardur already
uses for signed and hashed data.

Primary references:

- [RFC 8785 JSON Canonicalization Scheme](https://www.rfc-editor.org/rfc/rfc8785.html)
- [Python `fcntl.flock` documentation](https://docs.python.org/3/library/fcntl.html#fcntl.flock)
- [Python `os.replace` documentation](https://docs.python.org/3/library/os.html#os.replace)

## Decision

1. Spend authority is an optional signed Mission Passport claim distinct from
call counts. It declares exact metered tools, a currency, and integer token
and currency-micro ceilings for session, agent, and delegation-lineage
scopes.
2. Root issuance binds spend authority to the root passport JTI. Derived child
passports inherit the complete spend policy and lineage identifier; they
cannot change currency, metered tools, or ceilings.
3. Operators load immutable, validity-bounded quote snapshots into the proxy.
A caller supplies only quote ID, model ID, request ID, known maximum input
tokens, and configured maximum output tokens. The quote supplies rates,
currency, tool binding, model binding, and validity. No authorization-path
network fetch occurs.
4. Authorization uses non-negative integers no larger than the interoperable
JSON safe-integer bound. Monetary rates are currency micro-units per million
tokens; multiplication and ceiling division use integers only.
5. One file-locked transaction reserves both token and monetary upper bounds
across all three scopes before `evaluate_tool_call()` can return `PERMIT`.
A duplicate active request is denied rather than permitting a potentially
duplicated provider execution. Close operations must present the session
identity bound into the reservation; one sibling cannot settle, release, or
quarantine another sibling's authority.
6. Trusted settlement recomputes actual money from the originally bound quote.
Usage within the reservation consumes actual amounts and refunds only the
proven unused portion. Missing, invalid, or oversized usage is
quarantined with the conservative reservation retained.
7. Stale reservations can be moved to quarantine by their owning session and
later reconciled with trusted usage. There is no automatic timeout refund.
A session cannot finalize while it has an active reservation, and spend
lifecycle APIs cannot append evidence after session finalization.
8. Reservation decisions and settlement lifecycle operations emit separate,
signed, hash-linked receipts. Existing receipts are immutable. Public
evidence contains bounded amounts, remaining scopes, currency, quote and
request hashes, and reason codes; it excludes prompts, responses, raw quote
documents, account identifiers, credentials, request IDs, and host paths.
9. Metrics use only bounded operation/outcome/unit labels. The reference ledger
is local to one shared state directory; distributed deployments require an
external transactional implementation of the same ledger interface.
10. An exception after durable reservation but before authorization returns
triggers an idempotent release. If that compensation cannot be persisted,
evaluation fails with `spend_compensation_failed` and retains the full
reservation. A failure in receipt construction can prevent signed evidence
for that internal compensation, so the durable ledger remains the
fail-closed source of truth for recovery.

## Consequences

- A metered action cannot start after any declared scope is exhausted.
- Conservative output reservation can temporarily reject work that would have
fit after actual settlement. This is an intentional availability-for-safety
trade-off.
- Crashes and missing usage retain authority, so operators need stale
quarantine monitoring and a trusted reconciliation path.
- Adapters are in the trusted computing base for usage evidence. The ledger
prevents replay and oversubscription but cannot prove a provider's usage
report independently.
- Credentials without `spend_budget` retain existing call-count behavior.
- The reference implementation does not perform provider billing
reconciliation, chargeback, or cross-host consensus.

## Alternatives considered

- **Record provider cost after the call.** Rejected because it observes an
overspend after the external side effect and cannot enforce a pre-action cap.
- **Fetch live prices during evaluation.** Rejected because mutable remote data
would become authorization authority and introduce a network fail point.
- **Let the caller submit a rate or provider-selected model.** Rejected because
an untrusted caller could select a cheaper quote and widen effective spend.
- **Reuse the call-count lineage ledger.** Rejected because calls, tokens, and
currency have different units and settlement semantics.
- **Automatically release stale reservations.** Rejected because a timeout
does not prove that the provider call did not execute or incur cost.
- **Permit an active duplicate request idempotently.** Rejected because the
proxy cannot guarantee the downstream provider will deduplicate execution.
1 change: 1 addition & 0 deletions docs/decisions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ ADRs are migrated from the private research repo with the two-pass cleanup appli
| 022 | [SPIFFE mTLS identity for operator telemetry](./ADR-022-operator-telemetry-spiffe-mtls.md) | Accepted | 2026-07-11 |
| 023 | [Explicit resource-scope authority](./ADR-023-explicit-resource-scope-authority.md) | Accepted | 2026-07-12 |
| 024 | [Self-asserted owner identity assurance](./ADR-024-self-asserted-owner-identity-assurance.md) | Accepted | 2026-07-12 |
| 025 | [Pre-action spend reservation and conservative settlement](./ADR-025-pre-action-spend-reservation.md) | Accepted | 2026-07-14 |

## Conventions

Expand Down
3 changes: 3 additions & 0 deletions docs/reference/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ walkthroughs see [`../guides/`](../guides/); for protocol semantics see
- [Advisory AI Controls](advisory-ai-controls.md) — semantic-judge and
behavioral-fingerprint defaults, non-authoritative status, failure policy,
cost, and integration requirements
- [Pre-action Spend Budgets](spend-budgets.md) — signed token/currency policy,
operator quote snapshots, atomic reserve/settle/quarantine semantics,
evidence, metrics, and deployment limits
- [Agent Recognition Evaluation](agent-recognition-evaluation.md) — versioned
maintained corpus, deterministic metrics, Wilson intervals, CI thresholds,
and claim boundaries
Expand Down
204 changes: 204 additions & 0 deletions docs/reference/spend-budgets.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,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).
Comment on lines +1 to +204

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

7 changes: 7 additions & 0 deletions docs/specs/ardur-drp-mapping-v0.1.json
Original file line number Diff line number Diff line change
Expand Up @@ -465,6 +465,13 @@
"drp_path": "metadata.x-ardur.budget.maxToolCalls",
"rationale": "DRP has no action-count budget."
},
{
"source_surface": "legacy_python_passport",
"source_path": "spend_budget",
"classification": "extension",
"drp_path": "metadata.x-ardur.budget.spendBudget",
"rationale": "Token and monetary authority is security-critical. This mapping ledger reserves the extension path; the current DRP v0.1 emitter/profile does not project it and must fail closed rather than omit or downgrade the claim."
},
{
"source_surface": "legacy_python_passport",
"source_path": "max_duration_s",
Expand Down
2 changes: 1 addition & 1 deletion docs/specs/ardur-drp-mapping-v0.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ transformations are:
| `par_hash`, `parent_token_hash`, `parent_jti` | `parentReceiptId` plus Ardur audit fields | Resolve the actual profiled parent receipt. Token hashes and token IDs are retained but are not DRP receipt IDs. |
| `cnf.jwk` | `metadata.x-ardur.capabilityTokenRef.holderConfirmation.jwk` | The holder key is not the DRP receipt-signing key. |
| depth and delegation policy | `metadata.x-ardur.redelegation` | DRP describes depth behavior but has no Authorization Object fields for mode, depth, or maximum depth. |
| budgets and policy references | `metadata.x-ardur.budget`, `metadata.x-ardur.policy` | Security-critical extensions that participate in attenuation checks. |
| call-count/spend budgets and policy references | `metadata.x-ardur.budget`, `metadata.x-ardur.policy` | Security-critical extensions that participate in attenuation checks. The v0.1 mapping ledger reserves `spendBudget`, but the current DRP emitter/profile does not project it and must fail closed rather than omit or downgrade that authority. |
| `mission_ref` | `metadata.x-ardur.missionRef` | DRP instruction commitment does not replace the governing Mission Declaration reference. |

### 3.1. Critical Extension Rule
Expand Down
Loading
Loading