@agenttrust-sdk/mcp 0.2.5 → 0.3.0

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.
Files changed (51) hide show
  1. package/dist/embedded-docs/architecture.mdx +174 -0
  2. package/dist/embedded-docs/getting-started/quickstart.mdx +79 -56
  3. package/dist/embedded-docs/index.mdx +54 -37
  4. package/dist/embedded-docs/integration-guides/capability-namespaces.mdx +135 -8
  5. package/dist/embedded-docs/integration-guides/custom-attestor.mdx +169 -8
  6. package/dist/embedded-docs/integration-guides/dexter-adapter.mdx +76 -0
  7. package/dist/embedded-docs/integration-guides/facilitator-adapters.mdx +85 -41
  8. package/dist/embedded-docs/integration-guides/pay-sh-adapter.mdx +90 -54
  9. package/dist/embedded-docs/integration-guides/x402-facilitator.mdx +55 -24
  10. package/dist/embedded-docs/mcp/hosted-endpoint.mdx +197 -0
  11. package/dist/embedded-docs/mcp/index.mdx +108 -0
  12. package/dist/embedded-docs/mcp/install.mdx +183 -0
  13. package/dist/embedded-docs/mcp/prompts.mdx +90 -0
  14. package/dist/embedded-docs/mcp/resources.mdx +115 -0
  15. package/dist/embedded-docs/mcp/tools.mdx +156 -0
  16. package/dist/embedded-docs/programs/policy-vault/composer.mdx +117 -0
  17. package/dist/embedded-docs/programs/policy-vault/counterparty-tier-policy.mdx +81 -9
  18. package/dist/embedded-docs/programs/policy-vault/index.mdx +77 -47
  19. package/dist/embedded-docs/programs/policy-vault/kill-switch-policy.mdx +65 -8
  20. package/dist/embedded-docs/programs/policy-vault/require-validation-policy.mdx +76 -8
  21. package/dist/embedded-docs/programs/policy-vault/spending-policy.mdx +83 -8
  22. package/dist/embedded-docs/programs/policy-vault/velocity-policy.mdx +85 -8
  23. package/dist/embedded-docs/programs/trustgate.mdx +112 -30
  24. package/dist/embedded-docs/programs/validation-registry.mdx +139 -32
  25. package/dist/embedded-docs/reference/byte-offset-reference.mdx +102 -13
  26. package/dist/embedded-docs/reference/capability-namespaces.mdx +56 -0
  27. package/dist/embedded-docs/reference/changelog.mdx +230 -13
  28. package/dist/embedded-docs/reference/deny-reason-codes.mdx +86 -0
  29. package/dist/embedded-docs/reference/devnet-program-ids.mdx +50 -8
  30. package/dist/embedded-docs/reference/discriminator-constants.mdx +104 -10
  31. package/dist/embedded-docs/reference/mainnet-program-ids.mdx +89 -5
  32. package/dist/embedded-docs/reference/quantu-agent-registry.mdx +104 -9
  33. package/dist/embedded-docs/sdk/exports-reference.mdx +239 -0
  34. package/dist/embedded-docs/sdk/gate-payment.mdx +99 -14
  35. package/dist/embedded-docs/sdk/index.mdx +141 -40
  36. package/dist/embedded-docs/sdk/mount-trustgate.mdx +178 -8
  37. package/dist/embedded-docs/verification/adversarial-harness.mdx +88 -0
  38. package/dist/embedded-docs/verification/atomic-tx-invariant.mdx +141 -0
  39. package/dist/embedded-docs/verification/chained-validation.mdx +87 -0
  40. package/dist/embedded-docs/verification/devnet-smoke.mdx +85 -0
  41. package/dist/embedded-docs/verification/index.mdx +31 -0
  42. package/dist/embedded-docs/verification/kani-proofs.mdx +144 -0
  43. package/dist/embedded-docs/verification/live-evidence.mdx +180 -0
  44. package/dist/tools/read/get-quantu-reputation.d.ts +50 -11
  45. package/dist/tools/read/get-quantu-reputation.js +75 -26
  46. package/dist/tools/read/get-quantu-reputation.js.map +1 -1
  47. package/dist/tools/write/emit-feedback.d.ts +6 -0
  48. package/dist/tools/write/emit-feedback.js +12 -1
  49. package/dist/tools/write/emit-feedback.js.map +1 -1
  50. package/package.json +16 -15
  51. package/scripts/install-claude-desktop.sh +0 -0
@@ -1,68 +1,98 @@
1
1
  ---
2
2
  title: PolicyVault
3
- description: The policy composer that returns Allow, Deny, or RequireValidation.
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 decision engine for AgentTrust. It reads payer, payee, policy, velocity, kill-switch, and optional attestation data, then returns a `GateDecision`.
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
- `DenyReason::code()` is stable for clients even though the Borsh enum order is internal to Anchor.
16
+ **Devnet:** [`8Y6fGeNEHgmWmbt8JsRcF72jxbeBfJhomMjG6SuoJQTR`](https://explorer.solana.com/address/8Y6fGeNEHgmWmbt8JsRcF72jxbeBfJhomMjG6SuoJQTR?cluster=devnet)
39
17
 
40
- ## Policy kinds
18
+ ## Instructions
41
19
 
42
- | Policy | Inputs | State mutation on Allow |
43
- | --- | --- | --- |
44
- | Spending | `amount`, UTC day, ISO week | daily and weekly counters |
45
- | Velocity | `amount`, slot window, payer tier | `VelocityLedger.cumulative_amount` |
46
- | CounterpartyTier | payee tier, risk, confidence | none |
47
- | RequireValidation | subject, capability hash, attestor, expiry | none |
48
- | KillSwitch | per-agent paused flag | none |
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
- ## Safety rule
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
- The Anchor handler snapshots all accounts before composing, then applies returned deltas only when the decision is `Allow`.
31
+ ## State accounts
53
32
 
54
- ```rust
55
- match result.decision {
56
- GateDecision::Allow => {
57
- spending::apply_deltas(&mut ctx.accounts.policy_account, d);
58
- velocity::apply_deltas(&mut ctx.accounts.velocity_ledger, d);
59
- }
60
- GateDecision::Deny(_) | GateDecision::RequireValidation(_) => {}
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
- ## Formal checks
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
- Source: `programs/policy-vault/src/policies/composer.rs`.
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: Multisig-gated pause control for PolicyVault decisions.
3
+ description: First in the composer — a multisig-controlled emergency pause that blocks any Allow when paused.
4
4
  ---
5
5
 
6
- `In progress`
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
- KillSwitch is the first policy in the composer. When it is paused, the composer cannot return `Allow`.
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
- | Source | Path |
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: Capability-attestation checks for payments that need validation evidence.
3
+ description: Gate against a ValidationAttestation PDA capability hash, expiry, revocation, and per-policy attestor whitelist.
4
4
  ---
5
5
 
6
- `In progress`
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
- RequireValidation returns `RequireValidation` when the required capability is missing, and returns `Deny` when the supplied attestation is expired, revoked, or not issued by an accepted attestor.
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
- | Source | Path |
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 enforced before payment settlement.
3
+ description: Per-transaction, daily (UTC midnight), and weekly (ISO Monday) limits with anchor-rollover math.
4
4
  ---
5
5
 
6
- `In progress`
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
- The Spending policy gates a payment against configured per-transaction, daily, and weekly limits. Allow-path deltas are applied only after the full PolicyVault composer returns `Allow`.
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
- | Source | Path |
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 payment counters with tier-adjusted limits.
3
+ description: Sliding-window cumulative-spend counter with payer-tier-decayed window size and Allow-only ledger commit.
4
4
  ---
5
5
 
6
- `In progress`
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
- The Velocity policy applies a rolling counter before a payment can proceed. The composer carries velocity deltas only on the all-policies-allowed branch.
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
- | Source | Path |
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)