@agenttrust-sdk/mcp 0.2.6 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/embedded-data/devnet-smoke.json +4 -4
- package/dist/embedded-docs/architecture.mdx +174 -0
- package/dist/embedded-docs/getting-started/quickstart.mdx +79 -56
- package/dist/embedded-docs/index.mdx +54 -37
- package/dist/embedded-docs/integration-guides/capability-namespaces.mdx +135 -8
- package/dist/embedded-docs/integration-guides/custom-attestor.mdx +169 -8
- package/dist/embedded-docs/integration-guides/dexter-adapter.mdx +76 -0
- package/dist/embedded-docs/integration-guides/facilitator-adapters.mdx +85 -41
- package/dist/embedded-docs/integration-guides/pay-sh-adapter.mdx +90 -54
- package/dist/embedded-docs/integration-guides/x402-facilitator.mdx +55 -24
- package/dist/embedded-docs/mcp/hosted-endpoint.mdx +197 -0
- package/dist/embedded-docs/mcp/index.mdx +108 -0
- package/dist/embedded-docs/mcp/install.mdx +183 -0
- package/dist/embedded-docs/mcp/prompts.mdx +90 -0
- package/dist/embedded-docs/mcp/resources.mdx +115 -0
- package/dist/embedded-docs/mcp/tools.mdx +156 -0
- package/dist/embedded-docs/programs/policy-vault/composer.mdx +117 -0
- package/dist/embedded-docs/programs/policy-vault/counterparty-tier-policy.mdx +81 -9
- package/dist/embedded-docs/programs/policy-vault/index.mdx +77 -47
- package/dist/embedded-docs/programs/policy-vault/kill-switch-policy.mdx +65 -8
- package/dist/embedded-docs/programs/policy-vault/require-validation-policy.mdx +76 -8
- package/dist/embedded-docs/programs/policy-vault/spending-policy.mdx +83 -8
- package/dist/embedded-docs/programs/policy-vault/velocity-policy.mdx +85 -8
- package/dist/embedded-docs/programs/trustgate.mdx +112 -30
- package/dist/embedded-docs/programs/validation-registry.mdx +139 -32
- package/dist/embedded-docs/reference/byte-offset-reference.mdx +102 -13
- package/dist/embedded-docs/reference/capability-namespaces.mdx +56 -0
- package/dist/embedded-docs/reference/changelog.mdx +230 -13
- package/dist/embedded-docs/reference/deny-reason-codes.mdx +86 -0
- package/dist/embedded-docs/reference/devnet-program-ids.mdx +50 -8
- package/dist/embedded-docs/reference/discriminator-constants.mdx +104 -10
- package/dist/embedded-docs/reference/mainnet-program-ids.mdx +89 -5
- package/dist/embedded-docs/reference/quantu-agent-registry.mdx +104 -9
- package/dist/embedded-docs/sdk/exports-reference.mdx +239 -0
- package/dist/embedded-docs/sdk/gate-payment.mdx +99 -14
- package/dist/embedded-docs/sdk/index.mdx +141 -40
- package/dist/embedded-docs/sdk/mount-trustgate.mdx +178 -8
- package/dist/embedded-docs/verification/adversarial-harness.mdx +88 -0
- package/dist/embedded-docs/verification/atomic-tx-invariant.mdx +141 -0
- package/dist/embedded-docs/verification/chained-validation.mdx +87 -0
- package/dist/embedded-docs/verification/devnet-smoke.mdx +85 -0
- package/dist/embedded-docs/verification/index.mdx +31 -0
- package/dist/embedded-docs/verification/kani-proofs.mdx +144 -0
- package/dist/embedded-docs/verification/live-evidence.mdx +180 -0
- package/dist/tools/write/emit-feedback.d.ts +6 -0
- package/dist/tools/write/emit-feedback.js +12 -1
- package/dist/tools/write/emit-feedback.js.map +1 -1
- package/package.json +16 -15
- package/scripts/install-claude-desktop.sh +0 -0
|
@@ -1,15 +1,87 @@
|
|
|
1
1
|
---
|
|
2
|
-
title:
|
|
3
|
-
description: Quantu AtomStats
|
|
2
|
+
title: CounterpartyTier policy
|
|
3
|
+
description: Read Quantu's AtomStats by byte offset and gate the payment on tier, risk, and confidence — no Cargo dep on Quantu's crate.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
`
|
|
6
|
+
CounterpartyTier is the wedge. It reads the payee's `AtomStats` PDA (Quantu's `atom-engine` program) through a manual byte-offset parser, then checks `trust_tier`, `risk_score`, and `confidence` against the policy's thresholds.
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
Manual parsing — not Borsh deserialization through Quantu's crate. The parser is pinned to commit `bfb09ad` and includes a schema-version canary at byte 560 that fails loud rather than silently misreading fields if Quantu bumps its layout.
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
| --- | --- |
|
|
12
|
-
| evaluator | [`programs/policy-vault/src/policies/counterparty_tier.rs`](https://github.com/agenttrust-labs/agenttrust/blob/main/programs/policy-vault/src/policies/counterparty_tier.rs) |
|
|
13
|
-
| AtomStats parser | [`programs/policy-vault/src/ext/atom_engine.rs`](https://github.com/agenttrust-labs/agenttrust/blob/main/programs/policy-vault/src/ext/atom_engine.rs) |
|
|
14
|
-
| proof | [`programs/policy-vault/src/proofs/inv_counterparty_tier_monotone.rs`](https://github.com/agenttrust-labs/agenttrust/blob/main/programs/policy-vault/src/proofs/inv_counterparty_tier_monotone.rs) |
|
|
10
|
+
Source: [`programs/policy-vault/src/policies/counterparty_tier.rs`](https://github.com/agenttrust-labs/agenttrust/blob/main/programs/policy-vault/src/policies/counterparty_tier.rs). Parser: [`programs/policy-vault/src/ext/atom_engine.rs`](https://github.com/agenttrust-labs/agenttrust/blob/main/programs/policy-vault/src/ext/atom_engine.rs).
|
|
15
11
|
|
|
12
|
+
## Reads from `AtomStats`
|
|
13
|
+
|
|
14
|
+
| Offset | Width | Field | Notes |
|
|
15
|
+
|---:|---:|---|---|
|
|
16
|
+
| `549` | u8 | `risk_score` | 0..=255 — lower is better |
|
|
17
|
+
| `551` | u8 | `tier_immediate` | v1 demo default; 0..=4 |
|
|
18
|
+
| `555` | u8 | `tier_confirmed` | post-vesting; production mode preferred |
|
|
19
|
+
| `557` | u16 LE | `confidence` | basis points 0..=10000 |
|
|
20
|
+
| `560` | u8 | `schema_version` | canary — must equal 1 |
|
|
21
|
+
|
|
22
|
+
Account size canary: `ATOM_STATS_SIZE = 561`. Account-data-relative (the 8-byte Borsh discriminator at 0..8 is verified via owner + schema check, not by re-parsing).
|
|
23
|
+
|
|
24
|
+
`ATOM_TIER_MAX = 4`. A tier byte above 4 — with `schema_version == 1` — implies tampering or an undeclared spec change; the parser fails with `AtomStatsSchemaMismatch` rather than silently clamp. Same fail-loud rule for `tier_confirmed`.
|
|
25
|
+
|
|
26
|
+
Quantu pinned program ID (devnet): `AToMufS4QD6hEXvcvBDg9m1AHeCLpmZQsyfYa5h9MwAF`. Mainnet: [Reference → Mainnet program IDs](/reference/mainnet-program-ids).
|
|
27
|
+
|
|
28
|
+
## Policy state (subset of `PolicyAccount`)
|
|
29
|
+
|
|
30
|
+
```rust
|
|
31
|
+
pub gate_mode: u8, // off 49 — GATE_MODE_IMMEDIATE | _CONFIRMED
|
|
32
|
+
pub min_counterparty_tier: u8, // off 130
|
|
33
|
+
pub max_risk_score: u8, // off 131 — 255 = no constraint
|
|
34
|
+
pub min_confidence: u16, // off 132..134 — 0..=10000; 0 = no constraint
|
|
35
|
+
pub default_unrated_treatment: u8, // off 134 — UNRATED_DENY | _ALLOW | _REQUIRE_VALIDATION
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
`gate_mode` selects which tier byte is checked. `GATE_MODE_IMMEDIATE` reads byte 551 (the v1 demo default — fast Quantu fast-path tier). `GATE_MODE_CONFIRMED` reads byte 555 (post-vesting; production policies prefer this). Any other byte falls back to `tier_confirmed` — the conservative choice if `gate_mode` is corrupted.
|
|
39
|
+
|
|
40
|
+
## Decision
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
1. view = parse(atom_stats_account)
|
|
44
|
+
2. view == None → Unrated → composer applies default_unrated_treatment
|
|
45
|
+
3. tier < min_tier → Deny(CounterpartyTierBelowMin) code 6
|
|
46
|
+
4. max_risk_score < 255 AND view.risk_score > max_risk_score
|
|
47
|
+
→ Deny(CounterpartyRiskAboveMax) code 7
|
|
48
|
+
5. min_confidence > 0 AND view.confidence < min_confidence
|
|
49
|
+
→ Deny(CounterpartyConfidenceBelow) code 8
|
|
50
|
+
6. else → Allow
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The risk constraint is disabled when `max_risk_score == 255` (sentinel). The confidence constraint is disabled when `min_confidence == 0`. Both let a policy elect tier-only gating without touching the other fields.
|
|
54
|
+
|
|
55
|
+
## Unrated resolution
|
|
56
|
+
|
|
57
|
+
When the payee's `AtomStats` PDA is uninitialised (no rent or empty data), the parser returns `None` and the policy returns `Unrated`. The composer maps `Unrated` to one of three resolutions per `default_unrated_treatment`:
|
|
58
|
+
|
|
59
|
+
| Treatment | Constant | Composer maps to |
|
|
60
|
+
|---|---|---|
|
|
61
|
+
| `UNRATED_DENY` (default, byte `0`) | `0` | `Deny(UnratedTreatmentDeny)` — code 15 |
|
|
62
|
+
| `UNRATED_ALLOW` | `1` | `Allow` — proceed |
|
|
63
|
+
| `UNRATED_REQUIRE_VALIDATION` | `2` | `RequireValidation(capability_hash)` — facilitator routes user to attestation flow |
|
|
64
|
+
|
|
65
|
+
Unrecognised bytes fall through to `Deny` — the safe landing if on-chain corruption ever yields a value outside the documented range.
|
|
66
|
+
|
|
67
|
+
## Defensive failures (codes 9 / 10)
|
|
68
|
+
|
|
69
|
+
The parser also surfaces two defensive denials independent of policy thresholds:
|
|
70
|
+
|
|
71
|
+
- `AtomStatsWrongOwner` (code 9) — the PDA's owner is not Quantu's `atom-engine` program. Rejected via `require_keys_eq!` against the pinned `ATOM_ENGINE_ID`.
|
|
72
|
+
- `AtomStatsSchemaMismatch` (code 10) — size mismatch (`!= 561`), schema-version canary mismatch (`byte 560 != 1`), or tier byte above `ATOM_TIER_MAX = 4`.
|
|
73
|
+
|
|
74
|
+
Both are catastrophic-fail signals: a `Deny` here implies bad data, not a normal counterparty failure.
|
|
75
|
+
|
|
76
|
+
## Formal verification
|
|
77
|
+
|
|
78
|
+
- `counterparty_tier_monotone` (Kani #3, 8 sub-checks, 0.02 s) — if a STRICT policy (high `min_counterparty_tier`) produces `Allow` for a given payee, a LOOSER policy (lower or equal `min_tier`) on the same payee must also produce `Allow`. Loosening the tier requirement can never turn an `Allow` into a `Deny`.
|
|
79
|
+
|
|
80
|
+
In-module tests cover gate-mode selection (immediate vs confirmed), risk + confidence boundaries, sentinel disabling, fail-fast ordering (tier before risk before confidence), and unrated resolution for all four `UNRATED_*` values including unknown.
|
|
81
|
+
|
|
82
|
+
## Source
|
|
83
|
+
|
|
84
|
+
- Policy module: [`policies/counterparty_tier.rs`](https://github.com/agenttrust-labs/agenttrust/blob/main/programs/policy-vault/src/policies/counterparty_tier.rs)
|
|
85
|
+
- AtomStats parser: [`ext/atom_engine.rs`](https://github.com/agenttrust-labs/agenttrust/blob/main/programs/policy-vault/src/ext/atom_engine.rs)
|
|
86
|
+
- Kani proof: [`proofs/inv_counterparty_tier_monotone.rs`](https://github.com/agenttrust-labs/agenttrust/blob/main/programs/policy-vault/src/proofs/inv_counterparty_tier_monotone.rs)
|
|
87
|
+
- Byte-offset table: [Reference → Byte offsets](/reference/byte-offset-reference)
|
|
@@ -1,68 +1,98 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: PolicyVault
|
|
3
|
-
description: The policy
|
|
3
|
+
description: The decision engine — five orthogonal policy kinds composed under one gate_payment instruction with fail-fast semantics.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
PolicyVault is the
|
|
7
|
-
|
|
8
|
-
## Composer order
|
|
9
|
-
|
|
10
|
-
The order is fixed and fail-fast:
|
|
11
|
-
|
|
12
|
-
| Order | Policy | Why it runs there |
|
|
13
|
-
| --- | --- | --- |
|
|
14
|
-
| 1 | `KillSwitch` | cheapest global stop |
|
|
15
|
-
| 2 | `Spending` | pure amount and calendar bounds |
|
|
16
|
-
| 3 | `Velocity` | sliding-window spend bound keyed by payer tier |
|
|
17
|
-
| 4 | `CounterpartyTier` | reads payee AtomStats trust data |
|
|
18
|
-
| 5 | `RequireValidation` | reads the attestation PDA last |
|
|
19
|
-
|
|
20
|
-
```rust
|
|
21
|
-
pub fn compose_decision(input: ComposerInput) -> ComposerResult {
|
|
22
|
-
// KillSwitch -> Spending -> Velocity -> CounterpartyTier -> RequireValidation
|
|
23
|
-
// On Allow, deltas are returned for the Anchor wrapper to apply.
|
|
24
|
-
// On Deny or RequireValidation, deltas are None.
|
|
25
|
-
}
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
## Decision shape
|
|
6
|
+
PolicyVault is the program that turns "should this payment go through?" into a typed `GateDecision`. It reads the payer agent, the payee agent, the policy account, the velocity ledger, the kill switch, the payer's `AtomStats` (Quantu), the payee's `AtomStats`, and an optional `ValidationAttestation`, then returns one of three values:
|
|
29
7
|
|
|
30
8
|
```rust
|
|
31
9
|
pub enum GateDecision {
|
|
32
10
|
Allow,
|
|
33
11
|
Deny(DenyReason),
|
|
34
|
-
RequireValidation([u8; 32]),
|
|
12
|
+
RequireValidation([u8; 32]), // capability_hash
|
|
35
13
|
}
|
|
36
14
|
```
|
|
37
15
|
|
|
38
|
-
`
|
|
16
|
+
**Devnet:** [`8Y6fGeNEHgmWmbt8JsRcF72jxbeBfJhomMjG6SuoJQTR`](https://explorer.solana.com/address/8Y6fGeNEHgmWmbt8JsRcF72jxbeBfJhomMjG6SuoJQTR?cluster=devnet)
|
|
39
17
|
|
|
40
|
-
##
|
|
18
|
+
## Instructions
|
|
41
19
|
|
|
42
|
-
|
|
|
43
|
-
|
|
44
|
-
|
|
|
45
|
-
|
|
|
46
|
-
|
|
|
47
|
-
|
|
|
48
|
-
|
|
|
20
|
+
| Instruction | Effect |
|
|
21
|
+
|---|---|
|
|
22
|
+
| `init_authority` | Create `PolicyAuthority` PDA with multisig members + threshold |
|
|
23
|
+
| `init_killswitch` | Create `KillSwitchState` PDA (per-agent / per-collection / global) |
|
|
24
|
+
| `init_policy` | Create `PolicyAccount` + `VelocityLedger` for `(agent, policy_id)` |
|
|
25
|
+
| `set_killswitch` | Pause / unpause; multisig-gated against `PolicyAuthority` |
|
|
26
|
+
| `gate_payment` | Lazy decision — returns `Allow` / `Deny(reason)` / `RequireValidation(hash)` |
|
|
27
|
+
| `gate_payment_strict` | Strict variant — returns `Ok(())` iff `Allow`, else `Err`. The SDK's atomic composer uses this. |
|
|
49
28
|
|
|
50
|
-
|
|
29
|
+
The strict variant is the load-bearing one for atomicity. Phase J5 added a Kani proof, `gate_payment_strict_correctness`, that pins `strict_returns_ok_iff_allow` (a biconditional) and `gate_decision_is_one_of_three_disjoint_variants`. A future change that re-routes the `Deny` arm to an `Ok` return fails the proof loud.
|
|
51
30
|
|
|
52
|
-
|
|
31
|
+
## State accounts
|
|
53
32
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
```
|
|
33
|
+
| PDA | Seeds | Size | Role |
|
|
34
|
+
|---|---|---:|---|
|
|
35
|
+
| `PolicyAuthority` | `["policy_authority", agent_asset]` | 272 | Multisig members + threshold (1..=7) |
|
|
36
|
+
| `KillSwitchState` | `["killswitch", scope_kind, scope_key]` | 96 | Emergency pause flag + audit trail |
|
|
37
|
+
| `PolicyAccount` | `["policy", agent_asset, policy_id_le]` | 240 | Per-policy config + lazy counters |
|
|
38
|
+
| `VelocityLedger` | `["velocity", agent_asset, policy_id_le]` | 80 | Sliding-window cumulative-spend counter |
|
|
39
|
+
|
|
40
|
+
Per-program byte layouts live on each policy page (linked below). The full `PolicyAccount` declaration is in [`programs/policy-vault/src/state/policy_account.rs`](https://github.com/agenttrust-labs/agenttrust/blob/main/programs/policy-vault/src/state/policy_account.rs).
|
|
63
41
|
|
|
64
|
-
##
|
|
42
|
+
## The composer
|
|
43
|
+
|
|
44
|
+
`gate_payment` is a thin Anchor wrapper around the pure-Rust `compose_decision` function. The wrapper:
|
|
45
|
+
|
|
46
|
+
1. Snapshots all read accounts into plain Rust structs (`PolicySnapshot`, `VelocityLedgerSnapshot`, `KillSwitchSnapshot`, optional `AtomStatsView` ×2, optional `ValidationAttestationView`).
|
|
47
|
+
2. Calls `compose_decision(input)`.
|
|
48
|
+
3. On `Allow`, applies the returned `SpendingDeltas` and `VelocityDeltas` to `PolicyAccount` and `VelocityLedger` respectively.
|
|
49
|
+
4. On `Deny` or `RequireValidation`, mutates nothing.
|
|
50
|
+
|
|
51
|
+
Order is fixed and fail-fast:
|
|
52
|
+
|
|
53
|
+
| # | Policy | Cost | Fails fast on |
|
|
54
|
+
|---:|---|---|---|
|
|
55
|
+
| 1 | [KillSwitch](/programs/policy-vault/kill-switch-policy) | cheapest (one bool) | `paused == true` |
|
|
56
|
+
| 2 | [Spending](/programs/policy-vault/spending-policy) | pure arithmetic | per-tx / daily / weekly limits |
|
|
57
|
+
| 3 | [Velocity](/programs/policy-vault/velocity-policy) | one PDA read | sliding-window cap |
|
|
58
|
+
| 4 | [CounterpartyTier](/programs/policy-vault/counterparty-tier-policy) | two `AtomStats` PDA reads | tier / risk / confidence |
|
|
59
|
+
| 5 | [RequireValidation](/programs/policy-vault/require-validation-policy) | one attestation PDA read | subject / capability / expiry / attestor |
|
|
60
|
+
|
|
61
|
+
Full composer reference: [Composer](/programs/policy-vault/composer).
|
|
62
|
+
|
|
63
|
+
## DenyReason codes
|
|
64
|
+
|
|
65
|
+
`DenyReason` is the enum returned in the `Deny` arm. The Borsh wire format follows declaration order, but clients should consume the stable numeric `code()` instead — it is decoupled from Borsh field order.
|
|
66
|
+
|
|
67
|
+
| Code | Variant | Originating policy |
|
|
68
|
+
|---:|---|---|
|
|
69
|
+
| 1 | `KillSwitchEngaged` | KillSwitch |
|
|
70
|
+
| 2 | `SpendingPerTxExceeded` | Spending |
|
|
71
|
+
| 3 | `SpendingDailyExceeded` | Spending |
|
|
72
|
+
| 4 | `SpendingWeeklyExceeded` | Spending |
|
|
73
|
+
| 5 | `VelocityWindowExceeded` | Velocity |
|
|
74
|
+
| 6 | `CounterpartyTierBelowMin` | CounterpartyTier |
|
|
75
|
+
| 7 | `CounterpartyRiskAboveMax` | CounterpartyTier |
|
|
76
|
+
| 8 | `CounterpartyConfidenceBelow` | CounterpartyTier |
|
|
77
|
+
| 9 | `AtomStatsWrongOwner` | CounterpartyTier (defensive) |
|
|
78
|
+
| 10 | `AtomStatsSchemaMismatch` | CounterpartyTier (defensive) |
|
|
79
|
+
| 11 | `AttestationMissing` | RequireValidation |
|
|
80
|
+
| 12 | `AttestationExpired` | RequireValidation |
|
|
81
|
+
| 13 | `AttestationRevoked` | RequireValidation |
|
|
82
|
+
| 14 | `AttestationAttestorRejected` | RequireValidation |
|
|
83
|
+
| 15 | `UnratedTreatmentDeny` | CounterpartyTier (Unrated → Deny resolution) |
|
|
84
|
+
|
|
85
|
+
Full table with remediation hints: [Reference → DenyReason codes](/reference/deny-reason-codes).
|
|
86
|
+
|
|
87
|
+
## Formal verification
|
|
65
88
|
|
|
66
89
|
<KaniProofBadge />
|
|
67
90
|
|
|
68
|
-
|
|
91
|
+
Six Kani harnesses run on every PR via [`.github/workflows/kani-prove.yml`](https://github.com/agenttrust-labs/agenttrust/blob/main/.github/workflows/kani-prove.yml). Per-harness deep-dive: [Verification → Kani proofs](/verification/kani-proofs).
|
|
92
|
+
|
|
93
|
+
## Source
|
|
94
|
+
|
|
95
|
+
- Program entry: [`programs/policy-vault/src/lib.rs`](https://github.com/agenttrust-labs/agenttrust/blob/main/programs/policy-vault/src/lib.rs)
|
|
96
|
+
- Composer: [`programs/policy-vault/src/policies/composer.rs`](https://github.com/agenttrust-labs/agenttrust/blob/main/programs/policy-vault/src/policies/composer.rs)
|
|
97
|
+
- DenyReason: [`programs/policy-vault/src/state/decision.rs`](https://github.com/agenttrust-labs/agenttrust/blob/main/programs/policy-vault/src/state/decision.rs)
|
|
98
|
+
- Kani proofs: [`programs/policy-vault/src/proofs/`](https://github.com/agenttrust-labs/agenttrust/tree/main/programs/policy-vault/src/proofs)
|
|
@@ -1,15 +1,72 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: KillSwitch policy
|
|
3
|
-
description:
|
|
3
|
+
description: First in the composer — a multisig-controlled emergency pause that blocks any Allow when paused.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
`
|
|
6
|
+
KillSwitch is the cheapest policy and runs first. When the agent's `KillSwitchState.paused == true`, the composer returns `Deny(KillSwitchEngaged)` immediately — no other policy or foreign-PDA read costs are paid.
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
Source: [`programs/policy-vault/src/policies/killswitch.rs`](https://github.com/agenttrust-labs/agenttrust/blob/main/programs/policy-vault/src/policies/killswitch.rs).
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
| --- | --- |
|
|
12
|
-
| evaluator | [`programs/policy-vault/src/policies/killswitch.rs`](https://github.com/agenttrust-labs/agenttrust/blob/main/programs/policy-vault/src/policies/killswitch.rs) |
|
|
13
|
-
| pause instruction | [`programs/policy-vault/src/instructions/set_killswitch.rs`](https://github.com/agenttrust-labs/agenttrust/blob/main/programs/policy-vault/src/instructions/set_killswitch.rs) |
|
|
14
|
-
| proof | [`programs/policy-vault/src/proofs/inv_paused_implies_no_allow.rs`](https://github.com/agenttrust-labs/agenttrust/blob/main/programs/policy-vault/src/proofs/inv_paused_implies_no_allow.rs) |
|
|
10
|
+
## State
|
|
15
11
|
|
|
12
|
+
```rust
|
|
13
|
+
#[account]
|
|
14
|
+
pub struct KillSwitchState {
|
|
15
|
+
pub scope_kind: u8, // off 8 — Global / PerCollection / PerAgent
|
|
16
|
+
pub bump: u8, // off 9
|
|
17
|
+
pub paused: bool, // off 10 — the bit
|
|
18
|
+
pub _pad0: u8, // off 11
|
|
19
|
+
pub _pad1: [u8; 4], // off 12..16
|
|
20
|
+
pub scope_key: [u8; 32], // off 16..48 — zeros for Global
|
|
21
|
+
pub paused_at_slot: u64, // off 48..56
|
|
22
|
+
pub unpaused_at_slot: u64, // off 56..64
|
|
23
|
+
pub paused_by: Pubkey, // off 64..96
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
PDA seeds: `["killswitch", &[scope_kind], scope_key]`. Three scope kinds — global, per-collection, per-agent — all share the same struct.
|
|
28
|
+
|
|
29
|
+
## Mutation — `set_killswitch`
|
|
30
|
+
|
|
31
|
+
The pause flag flips through the `set_killswitch` instruction, which is multisig-gated against the agent's `PolicyAuthority`. The Anchor handler checks:
|
|
32
|
+
|
|
33
|
+
```rust
|
|
34
|
+
let distinct = authority.count_distinct_signing_members(&signer_keys);
|
|
35
|
+
require!(
|
|
36
|
+
distinct >= authority.threshold,
|
|
37
|
+
PolicyVaultError::ThresholdNotMet,
|
|
38
|
+
);
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
`count_distinct_signing_members` is pubkey-based: a single signer signing twice counts as one. Even if `PolicyAuthority.members` somehow contained duplicates (the `init_authority` constraint rejects this, but defense in depth), the count cannot exceed `min(member_count, signer_keys.len())`. That is the load-bearing property of `multisig_threshold_enforced` (Kani #5, 149 sub-checks, 77.17 s).
|
|
42
|
+
|
|
43
|
+
`PolicyAuthority` carries up to 7 members + a `threshold` (default 2). Source: [`programs/policy-vault/src/state/policy_authority.rs`](https://github.com/agenttrust-labs/agenttrust/blob/main/programs/policy-vault/src/state/policy_authority.rs).
|
|
44
|
+
|
|
45
|
+
## Pure decision function
|
|
46
|
+
|
|
47
|
+
```rust
|
|
48
|
+
pub fn evaluate(state: KillSwitchSnapshot) -> Option<DenyReason> {
|
|
49
|
+
if state.paused {
|
|
50
|
+
Some(DenyReason::KillSwitchEngaged)
|
|
51
|
+
} else {
|
|
52
|
+
None
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`Some(KillSwitchEngaged)` short-circuits the composer to `GateDecision::Deny(KillSwitchEngaged)` (DenyReason code `1`).
|
|
58
|
+
|
|
59
|
+
## Formal verification
|
|
60
|
+
|
|
61
|
+
`paused_implies_no_allow` (Kani #1, 126 sub-checks, 0.25 s) — if the agent's KillSwitch is `paused == true` AND `KIND_KILLSWITCH` is set in the policy bitmask, `compose_decision` cannot return `Allow` for any values of the other policy fields, ledger, or input parameters. The load-bearing safety invariant of the gate.
|
|
62
|
+
|
|
63
|
+
If a future change re-orders or skips KillSwitch evaluation, this proof fails loud. Reference: [Verification → Kani proofs](/verification/kani-proofs).
|
|
64
|
+
|
|
65
|
+
## Source
|
|
66
|
+
|
|
67
|
+
- Policy module: [`policies/killswitch.rs`](https://github.com/agenttrust-labs/agenttrust/blob/main/programs/policy-vault/src/policies/killswitch.rs)
|
|
68
|
+
- State: [`state/kill_switch_state.rs`](https://github.com/agenttrust-labs/agenttrust/blob/main/programs/policy-vault/src/state/kill_switch_state.rs)
|
|
69
|
+
- Mutation: [`instructions/set_killswitch.rs`](https://github.com/agenttrust-labs/agenttrust/blob/main/programs/policy-vault/src/instructions/set_killswitch.rs)
|
|
70
|
+
- Authority: [`state/policy_authority.rs`](https://github.com/agenttrust-labs/agenttrust/blob/main/programs/policy-vault/src/state/policy_authority.rs)
|
|
71
|
+
- Kani proof: [`proofs/inv_paused_implies_no_allow.rs`](https://github.com/agenttrust-labs/agenttrust/blob/main/programs/policy-vault/src/proofs/inv_paused_implies_no_allow.rs)
|
|
72
|
+
- Multisig Kani proof: [`proofs/inv_multisig_threshold_enforced.rs`](https://github.com/agenttrust-labs/agenttrust/blob/main/programs/policy-vault/src/proofs/inv_multisig_threshold_enforced.rs)
|
|
@@ -1,15 +1,83 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: RequireValidation policy
|
|
3
|
-
description:
|
|
3
|
+
description: Gate against a ValidationAttestation PDA — capability hash, expiry, revocation, and per-policy attestor whitelist.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
`
|
|
6
|
+
RequireValidation is the fifth and last policy. Reads one `ValidationAttestation` PDA from AgentTrust's `validation-registry` program, then checks subject, capability, revocation, expiry, and an optional per-policy attestor whitelist.
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
Source: [`programs/policy-vault/src/policies/require_validation.rs`](https://github.com/agenttrust-labs/agenttrust/blob/main/programs/policy-vault/src/policies/require_validation.rs). Parser: [`programs/policy-vault/src/ext/validation_registry.rs`](https://github.com/agenttrust-labs/agenttrust/blob/main/programs/policy-vault/src/ext/validation_registry.rs).
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
| --- | --- |
|
|
12
|
-
| evaluator | [`programs/policy-vault/src/policies/require_validation.rs`](https://github.com/agenttrust-labs/agenttrust/blob/main/programs/policy-vault/src/policies/require_validation.rs) |
|
|
13
|
-
| attestation parser | [`programs/policy-vault/src/ext/validation_registry.rs`](https://github.com/agenttrust-labs/agenttrust/blob/main/programs/policy-vault/src/ext/validation_registry.rs) |
|
|
14
|
-
| proof | [`programs/policy-vault/src/proofs/inv_validation_expiry_correct.rs`](https://github.com/agenttrust-labs/agenttrust/blob/main/programs/policy-vault/src/proofs/inv_validation_expiry_correct.rs) |
|
|
10
|
+
## Reads from `ValidationAttestation`
|
|
15
11
|
|
|
12
|
+
| Offset | Width | Field |
|
|
13
|
+
|---:|---:|---|
|
|
14
|
+
| `8` | Pubkey | `subject_asset` |
|
|
15
|
+
| `40` | [u8; 32] | `capability_hash` |
|
|
16
|
+
| `72` | Pubkey | `attestor` |
|
|
17
|
+
| `208` | u64 LE | `expires_at` (0 = never expires) |
|
|
18
|
+
| `216` | bool | `revoked` |
|
|
19
|
+
|
|
20
|
+
Account size: `VALIDATION_ATTESTATION_SIZE = 290`. The remaining bytes (signed message hash, claim URI hash, claim payload hash, issued_at, revocation reason hash, bump) are not read by the policy in v1 — they live on the PDA for audit-trail integrity but the parser only loads what the gate decides on.
|
|
21
|
+
|
|
22
|
+
ValidationRegistry pinned program ID (devnet): [`Cx4RFa6ysw3qXYhugPkF8pFSWBkmKq59h2dWgF2tKhtv`](https://explorer.solana.com/address/Cx4RFa6ysw3qXYhugPkF8pFSWBkmKq59h2dWgF2tKhtv?cluster=devnet).
|
|
23
|
+
|
|
24
|
+
## Policy state (subset of `PolicyAccount`)
|
|
25
|
+
|
|
26
|
+
```rust
|
|
27
|
+
pub required_capability_hash: [u8; 32], // off 135..167 — zeros = policy not enabled
|
|
28
|
+
pub accepted_attestors: [Pubkey; 2], // off 167..231 — both zeros = permissionless
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
The `required_capability_hash` is the SHA-256 of a UTF-8 capability name (e.g., `kyc.tier-1.v1` → `366c075140aa…ddc42`). When zero, the policy is effectively a pass-through. When set, every payment to this `(agent, policy_id)` requires an attestation matching that hash.
|
|
32
|
+
|
|
33
|
+
`accepted_attestors` is a 2-slot whitelist. Both zeros = permissionless (any registered attestor's signature flips the gate to `Allow`). Otherwise only attestations from those keys count.
|
|
34
|
+
|
|
35
|
+
## Decision
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
1. required_capability_hash == 0 → Allow (policy not configured)
|
|
39
|
+
2. attestation == None → RequireValidation(required_capability_hash)
|
|
40
|
+
3. view.subject_asset != payee_asset → Deny(AttestationMissing) code 11
|
|
41
|
+
4. view.capability_hash != required → Deny(AttestationMissing) code 11
|
|
42
|
+
5. view.revoked → Deny(AttestationRevoked) code 13
|
|
43
|
+
6. view.expires_at != 0 AND
|
|
44
|
+
view.expires_at <= now_slot → Deny(AttestationExpired) code 12
|
|
45
|
+
7. !permissionless AND attestor not in whitelist
|
|
46
|
+
→ Deny(AttestationAttestorRejected) code 14
|
|
47
|
+
8. else → Allow
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`expires_at == 0` is the "never expires" sentinel. `expires_at == now_slot` is treated as expired (the playbook bound: `expires_at > now` ⇒ not expired; equality fails).
|
|
51
|
+
|
|
52
|
+
When the attestation account is uninitialised (no rent or empty data), the policy returns `RequiresAttestation(capability_hash)` and the composer surfaces `GateDecision::RequireValidation(hash)` to the facilitator. The facilitator routes the user through the off-chain attestation flow → `request_validation` → attestor responds → `respond_to_validation` writes the PDA → re-submit payment → gate now reads the new attestation and returns `Allow`. End-to-end devnet trace: [Verification → Chained validation](/verification/chained-validation).
|
|
53
|
+
|
|
54
|
+
## Three-way outcome
|
|
55
|
+
|
|
56
|
+
```rust
|
|
57
|
+
pub enum RequireValidationOutcome {
|
|
58
|
+
Allow,
|
|
59
|
+
Deny(DenyReason),
|
|
60
|
+
RequiresAttestation([u8; 32]), // composer maps to RequireValidation
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
This is the only policy that returns a non-`Allow`/`Deny` shape. The composer's job is to bubble `RequiresAttestation(hash)` up as `GateDecision::RequireValidation(hash)` so the facilitator's `formatChallenge` can include the capability hash in the x402 response headers (`X-Capability-Required`).
|
|
65
|
+
|
|
66
|
+
## Per-policy attestor filtering
|
|
67
|
+
|
|
68
|
+
The v1 sybil-resistance model is "permissionless registration plus opinionated downstream filtering". Anyone can register as an attestor or a capability namespace. PolicyVault decides per-policy which attestors it trusts via `accepted_attestors[]`. A policy that gates against `audit.smart-contract.v1` might only accept attestations from Halborn or OtterSec; a policy that gates against `kyc.tier-1.v1` might accept any registered KYC attestor.
|
|
69
|
+
|
|
70
|
+
The trade-off is local trust over global gatekeeping — the only model that scales with the number of facilitators. Ten canonical v1 namespaces are already seeded on devnet: [Reference → Capability namespaces](/reference/capability-namespaces).
|
|
71
|
+
|
|
72
|
+
## Formal verification
|
|
73
|
+
|
|
74
|
+
- `validation_expiry_correct` (Kani #4, 85 sub-checks, 0.23 s) — an expired attestation (`expires_at != 0 AND expires_at <= now_slot`) cannot produce `Allow` from `require_validation::evaluate`. Subject + capability + revocation are forced equal so expiry is the deciding gate. Closes the obvious time-of-check / time-of-use stale-attestation hole.
|
|
75
|
+
|
|
76
|
+
In-module tests cover the zero-hash pass-through, missing-attestation `RequiresAttestation`, wrong-subject and wrong-capability denials, revocation, expiry boundaries (`expires_at == now`, `expires_at == 0`, `expires_at > now`), and whitelist with both 1-slot and 2-slot configurations.
|
|
77
|
+
|
|
78
|
+
## Source
|
|
79
|
+
|
|
80
|
+
- Policy module: [`policies/require_validation.rs`](https://github.com/agenttrust-labs/agenttrust/blob/main/programs/policy-vault/src/policies/require_validation.rs)
|
|
81
|
+
- ValidationAttestation parser: [`ext/validation_registry.rs`](https://github.com/agenttrust-labs/agenttrust/blob/main/programs/policy-vault/src/ext/validation_registry.rs)
|
|
82
|
+
- Kani proof: [`proofs/inv_validation_expiry_correct.rs`](https://github.com/agenttrust-labs/agenttrust/blob/main/programs/policy-vault/src/proofs/inv_validation_expiry_correct.rs)
|
|
83
|
+
- ValidationRegistry program: [ValidationRegistry](/programs/validation-registry)
|
|
@@ -1,15 +1,90 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Spending policy
|
|
3
|
-
description: Per-transaction, daily, and weekly limits
|
|
3
|
+
description: Per-transaction, daily (UTC midnight), and weekly (ISO Monday) limits with anchor-rollover math.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
Spending is the second policy in the composer. Pure arithmetic — no foreign PDA reads. Three constraints in order: per-tx max, daily max, weekly max.
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
Source: [`programs/policy-vault/src/policies/spending.rs`](https://github.com/agenttrust-labs/agenttrust/blob/main/programs/policy-vault/src/policies/spending.rs).
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
| --- | --- |
|
|
12
|
-
| evaluator | [`programs/policy-vault/src/policies/spending.rs`](https://github.com/agenttrust-labs/agenttrust/blob/main/programs/policy-vault/src/policies/spending.rs) |
|
|
13
|
-
| composer | [`programs/policy-vault/src/policies/composer.rs`](https://github.com/agenttrust-labs/agenttrust/blob/main/programs/policy-vault/src/policies/composer.rs) |
|
|
14
|
-
| policy init | [`programs/policy-vault/src/instructions/init_policy.rs`](https://github.com/agenttrust-labs/agenttrust/blob/main/programs/policy-vault/src/instructions/init_policy.rs) |
|
|
10
|
+
## State (subset of `PolicyAccount`)
|
|
15
11
|
|
|
12
|
+
```rust
|
|
13
|
+
pub spending_per_tx_max: u64, // off 50..58
|
|
14
|
+
pub spending_daily_max: u64, // off 58..66
|
|
15
|
+
pub spending_weekly_max: u64, // off 66..74
|
|
16
|
+
pub spending_today_used: u64, // off 74..82 — anchor-relative counter
|
|
17
|
+
pub spending_week_used: u64, // off 82..90 — anchor-relative counter
|
|
18
|
+
pub spending_today_anchor: u64, // off 90..98 — day index since 1970-01-01
|
|
19
|
+
pub spending_week_anchor: u64, // off 98..106 — week index since 1970-01-05 (Mon)
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Per-policy snapshots are extracted via `From<&PolicyAccount> for SpendingState`. The composer never mutates these fields directly — `apply_deltas` does, only on the `Allow` branch.
|
|
23
|
+
|
|
24
|
+
## Decision order
|
|
25
|
+
|
|
26
|
+
```rust
|
|
27
|
+
1. amount == 0 → Allow (no-op deltas)
|
|
28
|
+
2. amount > per_tx_max → Deny(SpendingPerTxExceeded) code 2
|
|
29
|
+
3. today_used + amount > daily_max → Deny(SpendingDailyExceeded) code 3
|
|
30
|
+
4. week_used + amount > weekly_max → Deny(SpendingWeeklyExceeded) code 4
|
|
31
|
+
5. else → Allow with new today/week counters + anchors
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
All comparisons use `checked_add`; overflow returns `Deny(SpendingDailyExceeded)` (the daily check is the first to overflow under realistic configs).
|
|
35
|
+
|
|
36
|
+
## Anchor rollover
|
|
37
|
+
|
|
38
|
+
`today_anchor` is `floor(unix_ts / 86_400)` — the day index since the Unix epoch. `week_anchor` is `floor((unix_ts - 4 × 86_400) / (7 × 86_400))` — the week index since Monday 1970-01-05 (the first Monday after the Unix epoch, which was a Thursday).
|
|
39
|
+
|
|
40
|
+
When the cluster's current `unix_ts` falls into a new day, the policy treats `today_used_effective = 0` (rollover); the same logic applies to weekly. The new anchor is written back along with the new counter on `Allow`.
|
|
41
|
+
|
|
42
|
+
```rust
|
|
43
|
+
let today_used_effective = if state.today_anchor == today_anchor_now {
|
|
44
|
+
state.today_used
|
|
45
|
+
} else {
|
|
46
|
+
0
|
|
47
|
+
};
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
The week anchor is biased to ISO Monday — `1970-01-05` was a Monday; subsequent weeks roll at Monday 00:00 UTC. The day anchor uses pure UTC midnight.
|
|
51
|
+
|
|
52
|
+
## Pure decision function
|
|
53
|
+
|
|
54
|
+
```rust
|
|
55
|
+
pub fn evaluate(state: SpendingState, amount: u64, unix_ts: i64) -> SpendingOutcome;
|
|
56
|
+
|
|
57
|
+
pub enum SpendingOutcome {
|
|
58
|
+
Allow(SpendingDeltas),
|
|
59
|
+
Deny(DenyReason),
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
pub struct SpendingDeltas {
|
|
63
|
+
pub new_today_used: u64,
|
|
64
|
+
pub new_week_used: u64,
|
|
65
|
+
pub new_today_anchor: u64,
|
|
66
|
+
pub new_week_anchor: u64,
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
The composer applies deltas via `spending::apply_deltas(account, &deltas)` only when every policy returns `Allow`. The deltas are computed but never written if a later policy denies — that is the rule that makes `velocity_counter_le_limit` (Kani #2) inductive.
|
|
71
|
+
|
|
72
|
+
## Formal verification
|
|
73
|
+
|
|
74
|
+
The Spending policy is exercised by the composer-level `gate_payment_strict_correctness` proof (Kani #6, 258 sub-checks). Spending-specific behaviour is covered by the 14 in-module unit tests, including:
|
|
75
|
+
|
|
76
|
+
- `happy_path_writes_amount_to_both_counters`
|
|
77
|
+
- `deny_per_tx_exceeded` / `deny_daily_exceeded_with_existing_usage` / `deny_weekly_exceeded_with_existing_usage`
|
|
78
|
+
- `rollover_resets_daily_when_anchor_changes` / `rollover_resets_weekly_when_anchor_changes`
|
|
79
|
+
- `overflow_is_treated_as_daily_exceeded`
|
|
80
|
+
- `boundary_amount_at_daily_max_allows`
|
|
81
|
+
- `negative_unix_ts_clamps_to_zero_anchors`
|
|
82
|
+
|
|
83
|
+
Source: [`programs/policy-vault/src/policies/spending.rs`](https://github.com/agenttrust-labs/agenttrust/blob/main/programs/policy-vault/src/policies/spending.rs) — 301 lines including tests.
|
|
84
|
+
|
|
85
|
+
## Source
|
|
86
|
+
|
|
87
|
+
- Policy module: [`policies/spending.rs`](https://github.com/agenttrust-labs/agenttrust/blob/main/programs/policy-vault/src/policies/spending.rs)
|
|
88
|
+
- Composer: [`policies/composer.rs`](https://github.com/agenttrust-labs/agenttrust/blob/main/programs/policy-vault/src/policies/composer.rs)
|
|
89
|
+
- State: [`state/policy_account.rs`](https://github.com/agenttrust-labs/agenttrust/blob/main/programs/policy-vault/src/state/policy_account.rs)
|
|
90
|
+
- Init instruction: [`instructions/init_policy.rs`](https://github.com/agenttrust-labs/agenttrust/blob/main/programs/policy-vault/src/instructions/init_policy.rs)
|
|
@@ -1,15 +1,92 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Velocity policy
|
|
3
|
-
description: Sliding-window
|
|
3
|
+
description: Sliding-window cumulative-spend counter with payer-tier-decayed window size and Allow-only ledger commit.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
`
|
|
6
|
+
Velocity is the third policy. One PDA read (`VelocityLedger`), then pure arithmetic. The window size is decayed by the payer's `trust_tier` so a tier-0 agent gets a tighter throttle than a tier-3 agent.
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
Source: [`programs/policy-vault/src/policies/velocity.rs`](https://github.com/agenttrust-labs/agenttrust/blob/main/programs/policy-vault/src/policies/velocity.rs).
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
| --- | --- |
|
|
12
|
-
| evaluator | [`programs/policy-vault/src/policies/velocity.rs`](https://github.com/agenttrust-labs/agenttrust/blob/main/programs/policy-vault/src/policies/velocity.rs) |
|
|
13
|
-
| account state | [`programs/policy-vault/src/state/velocity_ledger.rs`](https://github.com/agenttrust-labs/agenttrust/blob/main/programs/policy-vault/src/state/velocity_ledger.rs) |
|
|
14
|
-
| proof | [`programs/policy-vault/src/proofs/inv_velocity_counter_le_limit.rs`](https://github.com/agenttrust-labs/agenttrust/blob/main/programs/policy-vault/src/proofs/inv_velocity_counter_le_limit.rs) |
|
|
10
|
+
## `VelocityLedger` PDA
|
|
15
11
|
|
|
12
|
+
```rust
|
|
13
|
+
#[account]
|
|
14
|
+
pub struct VelocityLedger {
|
|
15
|
+
pub payer_agent_asset: Pubkey, // off 8..40
|
|
16
|
+
pub policy_id: u32, // off 40..44
|
|
17
|
+
pub bump: u8, // off 44
|
|
18
|
+
pub _pad0: [u8; 3], // off 45..48
|
|
19
|
+
pub cumulative_amount: u64, // off 48..56 — sum across active window
|
|
20
|
+
pub last_commit_slot: u64, // off 56..64 — slot of last Allow
|
|
21
|
+
pub window_start_slot: u64, // off 64..72 — first commit slot in current window
|
|
22
|
+
pub _reserved: [u8; 8], // off 72..80
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
PDA seeds: `["velocity", payer_agent_asset, policy_id_le_bytes]`. Account size: 80 bytes.
|
|
27
|
+
|
|
28
|
+
## Tier-decayed window
|
|
29
|
+
|
|
30
|
+
| Payer tier | Multiplier | Effective window |
|
|
31
|
+
|---:|---:|---|
|
|
32
|
+
| 0 (untrusted) | 1/4 | window × 0.25 |
|
|
33
|
+
| 1 | 2/4 | window × 0.50 |
|
|
34
|
+
| 2 | 3/4 | window × 0.75 |
|
|
35
|
+
| 3 (Gold, default) | 4/4 | window × 1.00 |
|
|
36
|
+
| 4 (Platinum) | 5/4 | window × 1.25 |
|
|
37
|
+
|
|
38
|
+
```rust
|
|
39
|
+
pub fn apply_tier_decay(base_secs: u64, payer_tier: u8) -> u64;
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Computed in `u128` to avoid overflow on tier-4 over very large bases; clamps to `u64::MAX` if the multiplied result overflows. Unknown tier (`tier > 4` — corruption canary) falls back to Gold (1×) — conservative-but-not-locking.
|
|
43
|
+
|
|
44
|
+
`SLOTS_PER_SECOND = 2` (the conservative envelope per `docs/plan/research/04-policyvault-build-playbook.md §E.1`). `window_slots = effective_window_secs × 2`.
|
|
45
|
+
|
|
46
|
+
## Decision
|
|
47
|
+
|
|
48
|
+
```rust
|
|
49
|
+
pub fn evaluate(
|
|
50
|
+
state: VelocityState,
|
|
51
|
+
ledger: VelocityLedgerSnapshot,
|
|
52
|
+
amount: u64,
|
|
53
|
+
payer_tier: u8,
|
|
54
|
+
now_slot: u64,
|
|
55
|
+
) -> VelocityOutcome;
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
1. amount == 0 → Allow with no-op deltas (last_commit advances, window not reset)
|
|
60
|
+
2. elapsed = saturating_sub(now_slot, window_start_slot)
|
|
61
|
+
3. if elapsed ≥ window_slots → window expired → reset cumulative to 0
|
|
62
|
+
4. new_cumulative = active_in_window + amount (checked_add → Deny on overflow)
|
|
63
|
+
5. if new_cumulative > max_in_window → Deny(VelocityWindowExceeded) code 5
|
|
64
|
+
6. else → Allow with new_cumulative + (maybe) new window_start_slot
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
`saturating_sub` on `now_slot` ensures clock-skew or replay scenarios where `now_slot < window_start_slot` clamp `elapsed` to 0 — window NOT expired, defensive.
|
|
68
|
+
|
|
69
|
+
## Allow-only commit
|
|
70
|
+
|
|
71
|
+
The Velocity policy never writes to `VelocityLedger` directly. The composer's Anchor wrapper applies `velocity::apply_deltas(&mut ledger, &deltas)` only when every prior + later policy returns `Allow`. That is what makes `velocity_counter_le_limit` (Kani #2) inductive: every prior `Allow` preserves `cumulative_amount ≤ max_in_window`; a fresh ledger trivially satisfies the base case.
|
|
72
|
+
|
|
73
|
+
```rust
|
|
74
|
+
pub struct VelocityDeltas {
|
|
75
|
+
pub new_cumulative_amount: u64,
|
|
76
|
+
pub new_last_commit_slot: u64,
|
|
77
|
+
pub new_window_start_slot: u64,
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Formal verification
|
|
82
|
+
|
|
83
|
+
- `velocity_counter_le_limit` (Kani #2, 9 sub-checks, 0.03 s) — if the pre-state ledger satisfies `cumulative_amount ≤ max_in_window`, then after `velocity::evaluate` returns `Allow(deltas)` the new cumulative counter still satisfies the bound. Cross-policy preservation against `spending.weekly_max` is a separate proof if/when needed.
|
|
84
|
+
|
|
85
|
+
In-module unit tests cover tier decay (5 cases), window expiry, boundary cases at `max_in_window`, overflow, `max_in_window == 0`, and `now_slot < window_start_slot`. Source: [`programs/policy-vault/src/policies/velocity.rs`](https://github.com/agenttrust-labs/agenttrust/blob/main/programs/policy-vault/src/policies/velocity.rs) — 339 lines.
|
|
86
|
+
|
|
87
|
+
## Source
|
|
88
|
+
|
|
89
|
+
- Policy module: [`policies/velocity.rs`](https://github.com/agenttrust-labs/agenttrust/blob/main/programs/policy-vault/src/policies/velocity.rs)
|
|
90
|
+
- State: [`state/velocity_ledger.rs`](https://github.com/agenttrust-labs/agenttrust/blob/main/programs/policy-vault/src/state/velocity_ledger.rs)
|
|
91
|
+
- Composer: [`policies/composer.rs`](https://github.com/agenttrust-labs/agenttrust/blob/main/programs/policy-vault/src/policies/composer.rs)
|
|
92
|
+
- Kani proof: [`proofs/inv_velocity_counter_le_limit.rs`](https://github.com/agenttrust-labs/agenttrust/blob/main/programs/policy-vault/src/proofs/inv_velocity_counter_le_limit.rs)
|