veralayer 0.1.0 → 0.2.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.
- package/README.md +113 -84
- package/dist/agent-kit.d.ts +27 -0
- package/dist/agent-kit.js +65 -0
- package/dist/cli.js +56 -14
- package/dist/client.d.ts +12 -3
- package/dist/client.js +40 -12
- package/dist/guard.d.ts +5 -1
- package/dist/guard.js +30 -18
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/inspect.d.ts +10 -4
- package/dist/inspect.js +56 -27
- package/dist/mcp.d.ts +4 -0
- package/dist/mcp.js +103 -0
- package/dist/policy.d.ts +35 -7
- package/dist/policy.js +62 -20
- package/package.json +25 -9
package/README.md
CHANGED
|
@@ -1,84 +1,113 @@
|
|
|
1
|
-
# veralayer
|
|
2
|
-
|
|
3
|
-
A payment guard for AI agents on Solana. It sits between your agent's wallet and the network and checks every outgoing transaction:
|
|
4
|
-
|
|
5
|
-
- **Decodes the transaction.** It finds where value goes: SOL transfers, SPL and Token-2022 transfers (each token account is resolved to the wallet that owns it), v0 lookup tables, and every program called.
|
|
6
|
-
- **Scores each recipient.** Every recipient is checked with [Vera](https://
|
|
7
|
-
- **Verifies the signature.** Each result is signed by Vera. The guard verifies that signature locally against Vera's published ed25519 key, so a tampered score is rejected.
|
|
8
|
-
- **Applies your policy.** It returns `allow`, `review` or `block`, with a finding for each rule that fired. Only allowed transactions are signed and sent.
|
|
9
|
-
|
|
10
|
-
```bash
|
|
11
|
-
npm install veralayer @solana/web3.js
|
|
12
|
-
```
|
|
13
|
-
|
|
14
|
-
## Guard a wallet
|
|
15
|
-
|
|
16
|
-
```ts
|
|
17
|
-
import { Connection } from '@solana/web3.js';
|
|
18
|
-
import { VeraGuard, VeraBlockedError } from 'veralayer';
|
|
19
|
-
|
|
20
|
-
const guard = new VeraGuard({
|
|
21
|
-
connection: new Connection(process.env.RPC_URL!),
|
|
22
|
-
policy: {
|
|
23
|
-
minTier: 'medium', // block low-trust recipients
|
|
24
|
-
tierLimitsSol: { high: 50, medium: 5 }, // max SOL per recipient per tx, by tier
|
|
25
|
-
dailyLimitSol: 20, // across all transactions today
|
|
26
|
-
tokenLimits: { EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v: 500 }, // 500 USDC per tx
|
|
27
|
-
deny: ['<known bad address>'],
|
|
28
|
-
allow: ['<your treasury>'],
|
|
29
|
-
},
|
|
30
|
-
onReview: async (decision) => askHumanInTelegram(decision), // optional; without it, review = block
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
try {
|
|
34
|
-
const { signature, decision } = await guard.sendTransaction(tx, [agentKeypair]);
|
|
35
|
-
} catch (e) {
|
|
36
|
-
if (e instanceof VeraBlockedError) console.log(e.decision.findings); // why it was stopped
|
|
37
|
-
}
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
## Decide before you build a transaction
|
|
41
|
-
|
|
42
|
-
```ts
|
|
43
|
-
const d = await guard.evaluatePayment({ to: seller, amountSol: 1.5 });
|
|
44
|
-
if (d.action !== 'allow') return refuse(d.findings[0].message);
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
## Just score an address
|
|
48
|
-
|
|
49
|
-
```ts
|
|
50
|
-
import { Vera } from 'veralayer';
|
|
51
|
-
const vera = new Vera();
|
|
52
|
-
const r = await vera.check(address); // { attestation: { score, tier, reasons, ... }, signature, summary }
|
|
53
|
-
await vera.verify(r); // { valid: true, expired: false }, checked offline
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
##
|
|
57
|
-
|
|
58
|
-
```
|
|
59
|
-
npx veralayer
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
##
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
1
|
+
# veralayer
|
|
2
|
+
|
|
3
|
+
A payment guard for AI agents on Solana. It sits between your agent's wallet and the network and checks every outgoing transaction:
|
|
4
|
+
|
|
5
|
+
- **Decodes the transaction.** It finds where value goes: SOL transfers, SPL and Token-2022 transfers (each token account is resolved to the wallet that owns it), v0 lookup tables, and every program called.
|
|
6
|
+
- **Scores each recipient.** Every recipient is checked with [Vera](https://veraagent.io). The score is built from on-chain history, and every reason cites the fact behind it.
|
|
7
|
+
- **Verifies the signature.** Each result is signed by Vera. The guard verifies that signature locally against Vera's published ed25519 key, so a tampered score is rejected.
|
|
8
|
+
- **Applies your policy.** It returns `allow`, `review` or `block`, with a finding for each rule that fired. Only allowed transactions are signed and sent.
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npm install veralayer @solana/web3.js
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Guard a wallet
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
import { Connection } from '@solana/web3.js';
|
|
18
|
+
import { VeraGuard, VeraBlockedError } from 'veralayer';
|
|
19
|
+
|
|
20
|
+
const guard = new VeraGuard({
|
|
21
|
+
connection: new Connection(process.env.RPC_URL!),
|
|
22
|
+
policy: {
|
|
23
|
+
minTier: 'medium', // block low-trust recipients
|
|
24
|
+
tierLimitsSol: { high: 50, medium: 5 }, // max SOL per recipient per tx, by tier
|
|
25
|
+
dailyLimitSol: 20, // across all transactions today
|
|
26
|
+
tokenLimits: { EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v: 500 }, // 500 USDC per tx
|
|
27
|
+
deny: ['<known bad address>'],
|
|
28
|
+
allow: ['<your treasury>'],
|
|
29
|
+
},
|
|
30
|
+
onReview: async (decision) => askHumanInTelegram(decision), // optional; without it, review = block
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const { signature, decision } = await guard.sendTransaction(tx, [agentKeypair]);
|
|
35
|
+
} catch (e) {
|
|
36
|
+
if (e instanceof VeraBlockedError) console.log(e.decision.findings); // why it was stopped
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Decide before you build a transaction
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
const d = await guard.evaluatePayment({ to: seller, amountSol: 1.5 });
|
|
44
|
+
if (d.action !== 'allow') return refuse(d.findings[0].message);
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Just score an address
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
import { Vera } from 'veralayer';
|
|
51
|
+
const vera = new Vera();
|
|
52
|
+
const r = await vera.check(address); // { attestation: { score, tier, reasons, ... }, signature, summary }
|
|
53
|
+
await vera.verify(r); // { valid: true, expired: false }, checked offline
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Use it from Claude, Cursor or any MCP client
|
|
57
|
+
|
|
58
|
+
```json
|
|
59
|
+
{ "mcpServers": { "vera": { "command": "npx", "args": ["-y", "veralayer", "mcp"] } } }
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Tools: `vera_check(address)` and `vera_guard_payment(to, amount_sol)`.
|
|
63
|
+
|
|
64
|
+
## Solana Agent Kit
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
import { createVeraPlugin } from 'veralayer/agent-kit';
|
|
68
|
+
agent.use(createVeraPlugin({ policy: { minTier: 'medium' } }));
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Full docs: https://veraagent.io/docs
|
|
72
|
+
|
|
73
|
+
## CLI
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
npx veralayer check <address>
|
|
77
|
+
npx veralayer pay <address> 2.5 --min-tier medium --daily 20
|
|
78
|
+
npx veralayer inspect <base64 transaction> --rpc <url>
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Policy reference
|
|
82
|
+
|
|
83
|
+
| Field | Default | Effect |
|
|
84
|
+
|---|---|---|
|
|
85
|
+
| `minTier` | `'medium'` | Recipients below this tier are blocked. |
|
|
86
|
+
| `tierLimitsSol` | `{ high: ∞, medium: 5, low: 0 }` | Max SOL to one recipient per transaction. Over the limit → `review`; a limit of 0 → `block`. |
|
|
87
|
+
| `maxSolPerTx` | none | Total SOL per transaction → `block`. |
|
|
88
|
+
| `dailyLimitSol` | none | Total SOL per UTC day (ledger) → `block`. |
|
|
89
|
+
| `tokenLimits` | none | Per-mint cap in UI units → `review`. |
|
|
90
|
+
| `allow` / `deny` | `[]` | Skip scoring / always block. |
|
|
91
|
+
| `onUnknown` | `'review'` | Recipients with no history. |
|
|
92
|
+
| `onCheckError` | `'block'` | API failure, bad signature, or a result for another address. |
|
|
93
|
+
| `unknownPrograms` | `'review'` | Programs outside the well-known list. |
|
|
94
|
+
| `knownPrograms` | `'review'` | Well-known programs that move value the guard doesn't decode (swaps). Set `'allow'` if your agent swaps. |
|
|
95
|
+
| `riskyInstructions` | `'block'` | `approve`, `setAuthority`, `closeAccount` to someone else, or `assign`, signed by the wallet. |
|
|
96
|
+
|
|
97
|
+
A policy with a typo throws when the guard is created, rather than silently weakening a check.
|
|
98
|
+
|
|
99
|
+
## Trust
|
|
100
|
+
|
|
101
|
+
- **Pinned key.** Results must be signed by Vera's production key, which ships as `VERA_ISSUER`. For a self-hosted Vera, pass `vera: { apiUrl, issuer }`.
|
|
102
|
+
- **Address check.** A result for a different address than the one requested is rejected.
|
|
103
|
+
- **Bad amounts.** NaN, negative and infinite amounts block.
|
|
104
|
+
- **Daily limits.** Spend is reserved before sending and released if the send fails. Concurrent sends are serialised.
|
|
105
|
+
- **Relayers.** If a relayer pays fees, pass `wallet` so that only the agent's own transfers count.
|
|
106
|
+
|
|
107
|
+
## What it does not do (yet)
|
|
108
|
+
|
|
109
|
+
- **Value inside other programs.** Value moved inside swaps and other CPIs is not decoded. Those programs are listed, and unknown ones trigger `review`.
|
|
110
|
+
- **On-chain anchoring.** Vera's results are signed but not yet anchored on-chain.
|
|
111
|
+
- **Guarantees.** A score reads history, not intent. It is a risk signal, not a guarantee.
|
|
112
|
+
|
|
113
|
+
MIT · by [VeraLayer](https://veralayer.io)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { Policy } from './policy.js';
|
|
3
|
+
type Action = {
|
|
4
|
+
name: string;
|
|
5
|
+
similes: string[];
|
|
6
|
+
description: string;
|
|
7
|
+
examples: {
|
|
8
|
+
input: Record<string, unknown>;
|
|
9
|
+
output: Record<string, unknown>;
|
|
10
|
+
explanation: string;
|
|
11
|
+
}[][];
|
|
12
|
+
schema: z.ZodTypeAny;
|
|
13
|
+
handler: (agent: unknown, input: Record<string, any>) => Promise<Record<string, any>>;
|
|
14
|
+
};
|
|
15
|
+
type Plugin = {
|
|
16
|
+
name: string;
|
|
17
|
+
methods: Record<string, any>;
|
|
18
|
+
actions: Action[];
|
|
19
|
+
initialize(agent: unknown): void;
|
|
20
|
+
};
|
|
21
|
+
export declare function createVeraPlugin(opts?: {
|
|
22
|
+
apiUrl?: string;
|
|
23
|
+
apiKey?: string;
|
|
24
|
+
issuer?: string;
|
|
25
|
+
policy?: Policy;
|
|
26
|
+
}): Plugin;
|
|
27
|
+
export default createVeraPlugin;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// Solana Agent Kit v2 plugin.
|
|
2
|
+
//
|
|
3
|
+
// import { SolanaAgentKit } from 'solana-agent-kit';
|
|
4
|
+
// import { createVeraPlugin } from 'veralayer/agent-kit';
|
|
5
|
+
// const agent = new SolanaAgentKit(wallet, rpcUrl, {}).use(createVeraPlugin({ policy: { minTier: 'medium' } }));
|
|
6
|
+
// await agent.methods.veraGuardPayment(to, 1.5);
|
|
7
|
+
//
|
|
8
|
+
// Types are structural, so this file needs zod but not solana-agent-kit itself.
|
|
9
|
+
import { z } from 'zod';
|
|
10
|
+
import { Vera } from './client.js';
|
|
11
|
+
import { VeraGuard } from './guard.js';
|
|
12
|
+
export function createVeraPlugin(opts = {}) {
|
|
13
|
+
const vera = new Vera(opts);
|
|
14
|
+
const guard = new VeraGuard({ vera, policy: opts.policy });
|
|
15
|
+
const check = async (address) => {
|
|
16
|
+
const r = await vera.check(address);
|
|
17
|
+
const v = await vera.verify(r);
|
|
18
|
+
const a = r.attestation;
|
|
19
|
+
return { address, score: a.score, tier: a.tier, reasons: a.reasons, summary: r.summary, attestation_id: r.id, signature_valid: v.valid };
|
|
20
|
+
};
|
|
21
|
+
const actions = [
|
|
22
|
+
{
|
|
23
|
+
name: 'VERA_TRUST_CHECK',
|
|
24
|
+
similes: ['check counterparty', 'is this address safe to pay', 'trust score', 'vet wallet before payment'],
|
|
25
|
+
description: 'Score a Solana address with Vera before sending it funds: 0-100 score, tier, reasons, and whether the signed result verified.',
|
|
26
|
+
examples: [[{ input: { address: '<counterparty address>' }, output: { status: 'success', score: 55, tier: 'medium' }, explanation: 'Illustrative: score a recipient before paying it' }]],
|
|
27
|
+
schema: z.object({ address: z.string().min(32).max(44) }),
|
|
28
|
+
handler: async (_agent, input) => {
|
|
29
|
+
try {
|
|
30
|
+
return { status: 'success', ...(await check(String(input.address))) };
|
|
31
|
+
}
|
|
32
|
+
catch (e) {
|
|
33
|
+
return { status: 'error', message: e.message };
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
name: 'VERA_GUARD_PAYMENT',
|
|
39
|
+
similes: ['should I pay', 'guard payment', 'check payment against policy'],
|
|
40
|
+
description: 'Decide whether a planned SOL payment may go ahead under the configured spending policy: allow, review or block, with the rule that fired.',
|
|
41
|
+
examples: [[{ input: { to: '<recipient>', amountSol: 1 }, output: { status: 'success', action: 'allow', findings: [] }, explanation: 'Illustrative: gate a 1 SOL payment' }]],
|
|
42
|
+
schema: z.object({ to: z.string().min(32).max(44), amountSol: z.number().positive() }),
|
|
43
|
+
handler: async (_agent, input) => {
|
|
44
|
+
try {
|
|
45
|
+
const d = await guard.evaluatePayment({ to: String(input.to), amountSol: Number(input.amountSol) });
|
|
46
|
+
return { status: 'success', action: d.action, findings: d.findings };
|
|
47
|
+
}
|
|
48
|
+
catch (e) {
|
|
49
|
+
return { status: 'error', message: e.message };
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
];
|
|
54
|
+
return {
|
|
55
|
+
name: 'vera',
|
|
56
|
+
methods: {
|
|
57
|
+
veraCheck: check,
|
|
58
|
+
veraRequire: (address, minTier = 'medium') => vera.require(address, minTier),
|
|
59
|
+
veraGuardPayment: (to, amountSol) => guard.evaluatePayment({ to, amountSol }),
|
|
60
|
+
},
|
|
61
|
+
actions,
|
|
62
|
+
initialize() { },
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
export default createVeraPlugin;
|
package/dist/cli.js
CHANGED
|
@@ -6,16 +6,51 @@
|
|
|
6
6
|
import { Vera } from './client.js';
|
|
7
7
|
import { VeraGuard } from './guard.js';
|
|
8
8
|
const args = process.argv.slice(2);
|
|
9
|
-
|
|
10
|
-
const
|
|
11
|
-
const pos =
|
|
9
|
+
// supports --name value and --name=value
|
|
10
|
+
const flags = new Map();
|
|
11
|
+
const pos = [];
|
|
12
|
+
for (let i = 0; i < args.length; i++) {
|
|
13
|
+
const a = args[i];
|
|
14
|
+
if (a === '--json') {
|
|
15
|
+
flags.set('json', '1');
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
if (a.startsWith('--')) {
|
|
19
|
+
const [k, v] = a.slice(2).split(/=(.*)/s);
|
|
20
|
+
if (v !== undefined)
|
|
21
|
+
flags.set(k, v);
|
|
22
|
+
else
|
|
23
|
+
flags.set(k, args[++i] ?? '');
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
pos.push(a);
|
|
27
|
+
}
|
|
28
|
+
const flag = (name) => flags.get(name);
|
|
29
|
+
const json = flags.has('json');
|
|
12
30
|
const [cmd, a1, a2] = pos;
|
|
31
|
+
const num = (name) => {
|
|
32
|
+
const v = flag(name);
|
|
33
|
+
if (v === undefined)
|
|
34
|
+
return undefined;
|
|
35
|
+
const n = Number(v);
|
|
36
|
+
if (!Number.isFinite(n) || n < 0) {
|
|
37
|
+
console.error(`--${name} must be a non-negative number`);
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
return n;
|
|
41
|
+
};
|
|
42
|
+
const minTier = flag('min-tier');
|
|
43
|
+
if (minTier !== undefined && !['high', 'medium', 'low'].includes(minTier)) {
|
|
44
|
+
console.error('--min-tier must be high, medium or low');
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
const daily = num('daily'), maxTx = num('max-tx');
|
|
13
48
|
const policy = {
|
|
14
|
-
...(
|
|
15
|
-
...(
|
|
16
|
-
...(
|
|
49
|
+
...(minTier ? { minTier: minTier } : {}),
|
|
50
|
+
...(daily !== undefined ? { dailyLimitSol: daily } : {}),
|
|
51
|
+
...(maxTx !== undefined ? { maxSolPerTx: maxTx } : {}),
|
|
17
52
|
};
|
|
18
|
-
const vera = new Vera({ apiUrl: flag('api') ?? process.env.VERA_API_URL, apiKey: process.env.VERA_API_KEY });
|
|
53
|
+
const vera = new Vera({ apiUrl: flag('api') ?? process.env.VERA_API_URL, apiKey: process.env.VERA_API_KEY, issuer: process.env.VERA_ISSUER });
|
|
19
54
|
const c = process.stdout.isTTY ? { r: '\x1b[31m', g: '\x1b[32m', y: '\x1b[33m', d: '\x1b[2m', b: '\x1b[1m', x: '\x1b[0m' } : { r: '', g: '', y: '', d: '', b: '', x: '' };
|
|
20
55
|
const tierColor = (t) => (t === 'high' ? c.g : t === 'medium' ? c.y : t === 'low' ? c.r : c.d);
|
|
21
56
|
const glyph = (k) => (k === 'ok' ? `${c.g}✓${c.x}` : k === 'warn' ? `${c.y}!${c.x}` : k === 'bad' ? `${c.r}×${c.x}` : `${c.d}i${c.x}`);
|
|
@@ -37,6 +72,10 @@ function printDecision(d) {
|
|
|
37
72
|
console.log(` ${c.d}no policy findings${c.x}`);
|
|
38
73
|
}
|
|
39
74
|
async function main() {
|
|
75
|
+
if (cmd === 'mcp') {
|
|
76
|
+
const { runMcp } = await import('./mcp.js');
|
|
77
|
+
return runMcp({ apiUrl: flag('api') ?? process.env.VERA_API_URL, apiKey: process.env.VERA_API_KEY });
|
|
78
|
+
}
|
|
40
79
|
if (cmd === 'check' && a1) {
|
|
41
80
|
const r = await vera.check(a1);
|
|
42
81
|
const v = await vera.verify(r);
|
|
@@ -52,6 +91,8 @@ async function main() {
|
|
|
52
91
|
return;
|
|
53
92
|
}
|
|
54
93
|
if (cmd === 'pay' && a1 && a2) {
|
|
94
|
+
if (!Number.isFinite(Number(a2)) || Number(a2) < 0)
|
|
95
|
+
throw new Error(`invalid amount: ${a2}`);
|
|
55
96
|
const d = await new VeraGuard({ vera, policy }).evaluatePayment({ to: a1, amountSol: Number(a2) });
|
|
56
97
|
return json ? console.log(JSON.stringify(d, null, 2)) : printDecision(d);
|
|
57
98
|
}
|
|
@@ -69,13 +110,14 @@ async function main() {
|
|
|
69
110
|
const d = await new VeraGuard({ vera, policy, connection }).evaluate(tx);
|
|
70
111
|
return json ? console.log(JSON.stringify(d, null, 2)) : printDecision(d);
|
|
71
112
|
}
|
|
72
|
-
console.log(`veralayer: payment guard for AI agents on Solana
|
|
73
|
-
|
|
74
|
-
veralayer check <address> score an address (signed, verified locally)
|
|
75
|
-
veralayer pay <address> <sol> decide on a planned SOL payment
|
|
76
|
-
veralayer inspect <base64 tx> decode a transaction and decide on it
|
|
77
|
-
|
|
78
|
-
|
|
113
|
+
console.log(`veralayer: payment guard for AI agents on Solana
|
|
114
|
+
|
|
115
|
+
veralayer check <address> score an address (signed, verified locally)
|
|
116
|
+
veralayer pay <address> <sol> decide on a planned SOL payment
|
|
117
|
+
veralayer inspect <base64 tx> decode a transaction and decide on it
|
|
118
|
+
veralayer mcp run as an MCP server over stdio
|
|
119
|
+
|
|
120
|
+
--min-tier high|medium|low --daily <sol> --max-tx <sol> --json
|
|
79
121
|
--api <url> (or VERA_API_URL) --rpc <url> (or SOLANA_RPC_URL)`);
|
|
80
122
|
process.exitCode = cmd ? 1 : 0;
|
|
81
123
|
}
|
package/dist/client.d.ts
CHANGED
|
@@ -34,6 +34,9 @@ export declare class VeraError extends Error {
|
|
|
34
34
|
status?: number | undefined;
|
|
35
35
|
constructor(message: string, status?: number | undefined);
|
|
36
36
|
}
|
|
37
|
+
/** Vera's production signing key. Results signed by anything else are rejected unless you pass your own `issuer`. */
|
|
38
|
+
export declare const VERA_ISSUER = "4VyCQLEb8eVmPYXDUwYkEpCMhXFBRshUiKHHuKTfPDFQ";
|
|
39
|
+
export declare const DEFAULT_API_URL = "https://veraagent.io";
|
|
37
40
|
export declare function canonical(v: unknown): string;
|
|
38
41
|
/** Verify an attestation offline. Pass the trusted Vera issuer key (from /.well-known/vera.json) to pin it. */
|
|
39
42
|
export declare function verifyAttestation(attestation: Attestation, signature: string, trustedIssuer?: string): VerifyResult;
|
|
@@ -41,20 +44,26 @@ export declare class Vera {
|
|
|
41
44
|
private apiUrl;
|
|
42
45
|
private apiKey?;
|
|
43
46
|
private issuer?;
|
|
47
|
+
private timeoutMs;
|
|
48
|
+
/**
|
|
49
|
+
* @param opts.issuer the key results must be signed with. Defaults to Vera's production key (VERA_ISSUER).
|
|
50
|
+
* Pass your own for a self-hosted Vera, or 'fetch' to trust /.well-known/vera.json on apiUrl.
|
|
51
|
+
*/
|
|
44
52
|
constructor(opts?: {
|
|
45
53
|
apiUrl?: string;
|
|
46
54
|
apiKey?: string;
|
|
47
55
|
issuer?: string;
|
|
56
|
+
timeoutMs?: number;
|
|
48
57
|
});
|
|
49
58
|
private req;
|
|
50
|
-
/** Score a counterparty. Returns score, tier, reasons and a signed attestation. */
|
|
59
|
+
/** Score a counterparty. Returns score, tier, reasons and a signed attestation. Throws if the result is for a different address. */
|
|
51
60
|
check(address: string, opts?: {
|
|
52
61
|
fresh?: boolean;
|
|
53
62
|
}): Promise<CheckResult>;
|
|
54
|
-
/** The
|
|
63
|
+
/** The key results must be signed with: pinned (default) or fetched once from /.well-known/vera.json. */
|
|
55
64
|
getIssuer(): Promise<string>;
|
|
56
65
|
/** Verify a check result locally against the trusted issuer key. */
|
|
57
66
|
verify(result: Pick<CheckResult, 'attestation' | 'signature'>): Promise<VerifyResult>;
|
|
58
|
-
/** Convenience gate: throws unless the
|
|
67
|
+
/** Convenience gate: throws unless the result verifies, has not expired and meets the minimum tier. */
|
|
59
68
|
require(address: string, minTier?: Exclude<Tier, 'unknown'>): Promise<CheckResult>;
|
|
60
69
|
}
|
package/dist/client.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Vera API client: score an address and verify the signed result locally against Vera's published key.
|
|
2
2
|
//
|
|
3
3
|
// import { Vera } from 'veralayer';
|
|
4
|
-
// const vera = new Vera()
|
|
4
|
+
// const vera = new Vera(); // hosted API, results must be signed with VERA_ISSUER
|
|
5
5
|
// const check = await vera.check(counterparty);
|
|
6
6
|
// if (!(await vera.verify(check)).valid) throw new Error('bad attestation');
|
|
7
7
|
// if (check.attestation.tier === 'low') throw new Error(check.attestation.reasons[0].text);
|
|
@@ -14,19 +14,28 @@ export class VeraError extends Error {
|
|
|
14
14
|
this.status = status;
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
|
+
/** Vera's production signing key. Results signed by anything else are rejected unless you pass your own `issuer`. */
|
|
18
|
+
export const VERA_ISSUER = '4VyCQLEb8eVmPYXDUwYkEpCMhXFBRshUiKHHuKTfPDFQ';
|
|
19
|
+
export const DEFAULT_API_URL = 'https://veraagent.io';
|
|
20
|
+
const TIERS = ['high', 'medium', 'low', 'unknown'];
|
|
17
21
|
// key-sorted JSON: must match the engine byte for byte
|
|
18
22
|
export function canonical(v) {
|
|
19
23
|
if (Array.isArray(v))
|
|
20
24
|
return `[${v.map(canonical).join(',')}]`;
|
|
21
25
|
if (v && typeof v === 'object')
|
|
22
26
|
return `{${Object.keys(v).sort().map((k) => `${JSON.stringify(k)}:${canonical(v[k])}`).join(',')}}`;
|
|
23
|
-
return JSON.stringify(v);
|
|
27
|
+
return JSON.stringify(v) ?? 'null';
|
|
24
28
|
}
|
|
25
29
|
/** Verify an attestation offline. Pass the trusted Vera issuer key (from /.well-known/vera.json) to pin it. */
|
|
26
30
|
export function verifyAttestation(attestation, signature, trustedIssuer) {
|
|
27
|
-
const
|
|
31
|
+
const exp = Date.parse(attestation?.expires_at);
|
|
32
|
+
const expired = !(exp > Date.now()); // an unparseable date counts as expired
|
|
33
|
+
if (!attestation || typeof attestation.issuer !== 'string')
|
|
34
|
+
return { valid: false, expired, reason: 'malformed attestation' };
|
|
28
35
|
if (trustedIssuer && attestation.issuer !== trustedIssuer)
|
|
29
36
|
return { valid: false, expired, reason: 'issuer is not the trusted Vera key' };
|
|
37
|
+
if (!TIERS.includes(attestation.tier))
|
|
38
|
+
return { valid: false, expired, reason: `unexpected tier ${String(attestation.tier)}` };
|
|
30
39
|
try {
|
|
31
40
|
const ok = ed25519.verify(bs58.decode(signature), new TextEncoder().encode(canonical(attestation)), bs58.decode(attestation.issuer));
|
|
32
41
|
return ok ? { valid: true, expired } : { valid: false, expired, reason: 'signature does not match' };
|
|
@@ -39,23 +48,38 @@ export class Vera {
|
|
|
39
48
|
apiUrl;
|
|
40
49
|
apiKey;
|
|
41
50
|
issuer;
|
|
51
|
+
timeoutMs;
|
|
52
|
+
/**
|
|
53
|
+
* @param opts.issuer the key results must be signed with. Defaults to Vera's production key (VERA_ISSUER).
|
|
54
|
+
* Pass your own for a self-hosted Vera, or 'fetch' to trust /.well-known/vera.json on apiUrl.
|
|
55
|
+
*/
|
|
42
56
|
constructor(opts = {}) {
|
|
43
|
-
this.apiUrl = (opts.apiUrl ??
|
|
57
|
+
this.apiUrl = (opts.apiUrl ?? DEFAULT_API_URL).replace(/\/$/, '');
|
|
44
58
|
this.apiKey = opts.apiKey;
|
|
45
|
-
this.issuer = opts.issuer;
|
|
59
|
+
this.issuer = opts.issuer === 'fetch' ? undefined : (opts.issuer ?? VERA_ISSUER);
|
|
60
|
+
this.timeoutMs = opts.timeoutMs ?? 30_000;
|
|
46
61
|
}
|
|
47
62
|
async req(path, init) {
|
|
48
|
-
|
|
63
|
+
let r;
|
|
64
|
+
try {
|
|
65
|
+
r = await fetch(this.apiUrl + path, { ...init, signal: AbortSignal.timeout(this.timeoutMs), headers: { 'content-type': 'application/json', ...(this.apiKey ? { 'x-api-key': this.apiKey } : {}), ...init?.headers } });
|
|
66
|
+
}
|
|
67
|
+
catch (e) {
|
|
68
|
+
throw new VeraError(`Vera API unreachable: ${e.message}`);
|
|
69
|
+
}
|
|
49
70
|
const j = (await r.json().catch(() => ({})));
|
|
50
71
|
if (!r.ok)
|
|
51
72
|
throw new VeraError(j.error || `Vera API error ${r.status}`, r.status);
|
|
52
73
|
return j;
|
|
53
74
|
}
|
|
54
|
-
/** Score a counterparty. Returns score, tier, reasons and a signed attestation. */
|
|
55
|
-
check(address, opts = {}) {
|
|
56
|
-
|
|
75
|
+
/** Score a counterparty. Returns score, tier, reasons and a signed attestation. Throws if the result is for a different address. */
|
|
76
|
+
async check(address, opts = {}) {
|
|
77
|
+
const r = await this.req('/v1/check', { method: 'POST', body: JSON.stringify({ address, fresh: opts.fresh }) });
|
|
78
|
+
if (r?.attestation?.subject !== address)
|
|
79
|
+
throw new VeraError('Vera returned a result for a different address');
|
|
80
|
+
return r;
|
|
57
81
|
}
|
|
58
|
-
/** The
|
|
82
|
+
/** The key results must be signed with: pinned (default) or fetched once from /.well-known/vera.json. */
|
|
59
83
|
async getIssuer() {
|
|
60
84
|
this.issuer ??= (await this.req('/.well-known/vera.json')).issuer;
|
|
61
85
|
return this.issuer;
|
|
@@ -64,13 +88,17 @@ export class Vera {
|
|
|
64
88
|
async verify(result) {
|
|
65
89
|
return verifyAttestation(result.attestation, result.signature, await this.getIssuer());
|
|
66
90
|
}
|
|
67
|
-
/** Convenience gate: throws unless the
|
|
91
|
+
/** Convenience gate: throws unless the result verifies, has not expired and meets the minimum tier. */
|
|
68
92
|
async require(address, minTier = 'medium') {
|
|
93
|
+
const rank = { unknown: 0, low: 1, medium: 2, high: 3 };
|
|
94
|
+
if (!(minTier in rank))
|
|
95
|
+
throw new VeraError(`invalid minTier: ${minTier}`);
|
|
69
96
|
const r = await this.check(address);
|
|
70
97
|
const v = await this.verify(r);
|
|
71
98
|
if (!v.valid)
|
|
72
99
|
throw new VeraError(`attestation invalid: ${v.reason}`);
|
|
73
|
-
|
|
100
|
+
if (v.expired)
|
|
101
|
+
throw new VeraError('attestation expired');
|
|
74
102
|
if (rank[r.attestation.tier] < rank[minTier])
|
|
75
103
|
throw new VeraError(`counterparty tier ${r.attestation.tier} is below ${minTier}: ${r.attestation.reasons[0]?.text ?? ''}`);
|
|
76
104
|
return r;
|
package/dist/guard.d.ts
CHANGED
|
@@ -23,6 +23,8 @@ export declare class VeraReviewRequiredError extends Error {
|
|
|
23
23
|
}
|
|
24
24
|
export type GuardOptions = {
|
|
25
25
|
connection?: Connection;
|
|
26
|
+
/** The wallet being guarded. Default: each transaction's fee payer. Set it when a relayer pays fees for your agent. */
|
|
27
|
+
wallet?: string;
|
|
26
28
|
policy?: Policy;
|
|
27
29
|
vera?: Vera | {
|
|
28
30
|
apiUrl?: string;
|
|
@@ -39,9 +41,11 @@ export declare class VeraGuard {
|
|
|
39
41
|
readonly vera: Vera;
|
|
40
42
|
readonly policy: Policy;
|
|
41
43
|
private connection?;
|
|
44
|
+
private wallet?;
|
|
42
45
|
private ledger;
|
|
43
46
|
private onReview?;
|
|
44
47
|
private onDecision?;
|
|
48
|
+
private lock;
|
|
45
49
|
constructor(opts?: GuardOptions);
|
|
46
50
|
/** Score recipients (deduplicated, in parallel) and verify each signed result against Vera's published key. */
|
|
47
51
|
checkRecipients(addresses: string[]): Promise<RecipientCheck[]>;
|
|
@@ -59,7 +63,7 @@ export declare class VeraGuard {
|
|
|
59
63
|
private decide;
|
|
60
64
|
/** Throws VeraBlockedError / VeraReviewRequiredError unless the transaction may be sent. */
|
|
61
65
|
assert(tx: Transaction | VersionedTransaction): Promise<Decision>;
|
|
62
|
-
/** Evaluate, then sign and send only if allowed.
|
|
66
|
+
/** Evaluate, then sign and send only if allowed. The SOL is reserved in the ledger before sending and released if sending fails. */
|
|
63
67
|
sendTransaction(tx: Transaction | VersionedTransaction, signers?: Signer[], options?: SendOptions): Promise<{
|
|
64
68
|
signature: string;
|
|
65
69
|
decision: Decision;
|
package/dist/guard.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Vera } from './client.js';
|
|
2
2
|
import { inspectTransaction } from './inspect.js';
|
|
3
|
-
import { evaluatePolicy } from './policy.js';
|
|
3
|
+
import { evaluatePolicy, validatePolicy } from './policy.js';
|
|
4
4
|
export class MemoryLedger {
|
|
5
5
|
day = '';
|
|
6
6
|
spent = 0;
|
|
@@ -29,13 +29,18 @@ export class VeraGuard {
|
|
|
29
29
|
vera;
|
|
30
30
|
policy;
|
|
31
31
|
connection;
|
|
32
|
+
wallet;
|
|
32
33
|
ledger;
|
|
33
34
|
onReview;
|
|
34
35
|
onDecision;
|
|
36
|
+
// evaluate + reserve run one at a time, so two concurrent sends can't both pass a daily limit
|
|
37
|
+
lock = Promise.resolve();
|
|
35
38
|
constructor(opts = {}) {
|
|
36
39
|
this.vera = opts.vera instanceof Vera ? opts.vera : new Vera(opts.vera);
|
|
37
40
|
this.policy = opts.policy ?? {};
|
|
41
|
+
validatePolicy(this.policy); // fail at startup on typos, not silently at payment time
|
|
38
42
|
this.connection = opts.connection;
|
|
43
|
+
this.wallet = opts.wallet;
|
|
39
44
|
this.ledger = opts.ledger ?? new MemoryLedger();
|
|
40
45
|
this.onReview = opts.onReview;
|
|
41
46
|
this.onDecision = opts.onDecision;
|
|
@@ -46,7 +51,7 @@ export class VeraGuard {
|
|
|
46
51
|
const uniq = [...new Set(addresses)].filter((a) => !skip.has(a));
|
|
47
52
|
return Promise.all(uniq.map(async (address) => {
|
|
48
53
|
try {
|
|
49
|
-
const r = await this.vera.check(address);
|
|
54
|
+
const r = await this.vera.check(address); // throws if the result is for another address
|
|
50
55
|
const v = await this.vera.verify(r);
|
|
51
56
|
const a = r.attestation;
|
|
52
57
|
return { address, ok: true, score: a.score, tier: a.tier, reasons: a.reasons, attestationId: r.id, verified: v.valid, expired: v.expired };
|
|
@@ -61,16 +66,16 @@ export class VeraGuard {
|
|
|
61
66
|
const outflows = payment.token
|
|
62
67
|
? [{ kind: 'token', to: payment.to, toAccount: payment.to, amount: payment.token.amount, mint: payment.token.mint, instruction: 'planned' }]
|
|
63
68
|
: [{ kind: 'sol', to: payment.to, toAccount: payment.to, amount: payment.amountSol ?? 0, instruction: 'planned' }];
|
|
64
|
-
return this.decide(outflows,
|
|
69
|
+
return this.decide(outflows, {});
|
|
65
70
|
}
|
|
66
71
|
/** Decode a transaction and decide on it. Does not send anything. */
|
|
67
72
|
async evaluate(tx) {
|
|
68
|
-
const ins = await inspectTransaction(tx, this.connection);
|
|
69
|
-
return this.decide(ins.outflows, ins.unknownPrograms);
|
|
73
|
+
const ins = await inspectTransaction(tx, this.connection, { wallet: this.wallet });
|
|
74
|
+
return this.decide(ins.outflows, { unknownPrograms: ins.unknownPrograms, valuePrograms: ins.valuePrograms, risky: ins.risky });
|
|
70
75
|
}
|
|
71
|
-
async decide(outflows,
|
|
76
|
+
async decide(outflows, extra) {
|
|
72
77
|
const recipients = await this.checkRecipients(outflows.map((o) => o.to));
|
|
73
|
-
const decision = evaluatePolicy({ outflows, recipients,
|
|
78
|
+
const decision = evaluatePolicy({ outflows, recipients, ...extra, spentTodaySol: await this.ledger.spentToday(), policy: this.policy });
|
|
74
79
|
this.onDecision?.(decision);
|
|
75
80
|
return decision;
|
|
76
81
|
}
|
|
@@ -83,21 +88,28 @@ export class VeraGuard {
|
|
|
83
88
|
throw new VeraReviewRequiredError(d);
|
|
84
89
|
return d;
|
|
85
90
|
}
|
|
86
|
-
/** Evaluate, then sign and send only if allowed.
|
|
91
|
+
/** Evaluate, then sign and send only if allowed. The SOL is reserved in the ledger before sending and released if sending fails. */
|
|
87
92
|
async sendTransaction(tx, signers = [], options) {
|
|
88
93
|
if (!this.connection)
|
|
89
94
|
throw new Error('VeraGuard needs a Connection to send transactions');
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
95
|
+
const run = this.lock.then(async () => { const d = await this.assert(tx); await this.ledger.add(d.totalSol); return d; });
|
|
96
|
+
this.lock = run.catch(() => { });
|
|
97
|
+
const decision = await run;
|
|
98
|
+
try {
|
|
99
|
+
let signature;
|
|
100
|
+
if ('version' in tx) {
|
|
101
|
+
if (signers.length)
|
|
102
|
+
tx.sign(signers);
|
|
103
|
+
signature = await this.connection.sendTransaction(tx, options);
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
signature = await this.connection.sendTransaction(tx, signers, options);
|
|
107
|
+
}
|
|
108
|
+
return { signature, decision };
|
|
96
109
|
}
|
|
97
|
-
|
|
98
|
-
|
|
110
|
+
catch (e) {
|
|
111
|
+
await this.ledger.add(-decision.totalSol);
|
|
112
|
+
throw e;
|
|
99
113
|
}
|
|
100
|
-
await this.ledger.add(decision.totalSol);
|
|
101
|
-
return { signature, decision };
|
|
102
114
|
}
|
|
103
115
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export { Vera, VeraError, verifyAttestation, canonical, type Attestation, type CheckResult, type Tier, type VerifyResult } from './client.js';
|
|
1
|
+
export { Vera, VeraError, VERA_ISSUER, DEFAULT_API_URL, verifyAttestation, canonical, type Attestation, type CheckResult, type Tier, type VerifyResult } from './client.js';
|
|
2
2
|
export { VeraGuard, MemoryLedger, VeraBlockedError, VeraReviewRequiredError, type GuardOptions, type SpendLedger } from './guard.js';
|
|
3
3
|
export { inspectTransaction, instructionsOf, type Inspection } from './inspect.js';
|
|
4
|
-
export { evaluatePolicy, DEFAULT_POLICY, type Policy, type Decision, type Finding, type Outflow, type RecipientCheck, type Action } from './policy.js';
|
|
4
|
+
export { evaluatePolicy, validatePolicy, DEFAULT_POLICY, type Policy, type RiskyInstruction, type Decision, type Finding, type Outflow, type RecipientCheck, type Action } from './policy.js';
|
|
5
5
|
export { KNOWN_PROGRAMS } from './programs.js';
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// veralayer: payment guard for AI agents on Solana.
|
|
2
|
-
export { Vera, VeraError, verifyAttestation, canonical } from './client.js';
|
|
2
|
+
export { Vera, VeraError, VERA_ISSUER, DEFAULT_API_URL, verifyAttestation, canonical } from './client.js';
|
|
3
3
|
export { VeraGuard, MemoryLedger, VeraBlockedError, VeraReviewRequiredError } from './guard.js';
|
|
4
4
|
export { inspectTransaction, instructionsOf } from './inspect.js';
|
|
5
|
-
export { evaluatePolicy, DEFAULT_POLICY } from './policy.js';
|
|
5
|
+
export { evaluatePolicy, validatePolicy, DEFAULT_POLICY } from './policy.js';
|
|
6
6
|
export { KNOWN_PROGRAMS } from './programs.js';
|
package/dist/inspect.d.ts
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import type { Connection, Transaction, VersionedTransaction } from '@solana/web3.js';
|
|
2
|
-
import type { Outflow } from './policy.js';
|
|
2
|
+
import type { Outflow, RiskyInstruction } from './policy.js';
|
|
3
3
|
export type Inspection = {
|
|
4
4
|
payer: string;
|
|
5
|
+
/** the wallet being guarded (option `wallet`, default the fee payer) */
|
|
6
|
+
wallet: string;
|
|
5
7
|
outflows: Outflow[];
|
|
6
8
|
programs: string[];
|
|
7
9
|
unknownPrograms: string[];
|
|
8
|
-
/**
|
|
10
|
+
/** names of well-known programs called that can move value the guard does not decode */
|
|
11
|
+
valuePrograms: string[];
|
|
12
|
+
risky: RiskyInstruction[];
|
|
9
13
|
notes: string[];
|
|
10
14
|
};
|
|
11
15
|
type Ix = {
|
|
@@ -18,6 +22,8 @@ export declare function instructionsOf(tx: Transaction | VersionedTransaction, c
|
|
|
18
22
|
payer: string;
|
|
19
23
|
ixs: Ix[];
|
|
20
24
|
}>;
|
|
21
|
-
/** Decode
|
|
22
|
-
export declare function inspectTransaction(tx: Transaction | VersionedTransaction, connection?: Connection
|
|
25
|
+
/** Decode what leaves `wallet` (default: the fee payer). Token destinations are resolved to their owner via RPC when needed. */
|
|
26
|
+
export declare function inspectTransaction(tx: Transaction | VersionedTransaction, connection?: Connection, opts?: {
|
|
27
|
+
wallet?: string;
|
|
28
|
+
}): Promise<Inspection>;
|
|
23
29
|
export {};
|
package/dist/inspect.js
CHANGED
|
@@ -3,9 +3,12 @@ const SYSTEM = '11111111111111111111111111111111';
|
|
|
3
3
|
const TOKEN = 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA';
|
|
4
4
|
const TOKEN22 = 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb';
|
|
5
5
|
const ATA = 'ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL';
|
|
6
|
-
|
|
6
|
+
// programs that never move value on their own
|
|
7
|
+
const INERT = new Set([SYSTEM, TOKEN, TOKEN22, ATA, 'ComputeBudget111111111111111111111111111111', 'MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr',
|
|
8
|
+
'Memo1UhkJRfHyvLMcVucJwxXeuD728EqVDDwQDxFMNo', 'noopb9bkMVfRPU8AsbpTUg8AQkHtKwMYZiFUjNRtMmV', 'AddressLookupTab1e1111111111111111111111111']);
|
|
7
9
|
const u64 = (d, o) => Number(new DataView(d.buffer, d.byteOffset + o, 8).getBigUint64(0, true));
|
|
8
10
|
const u32 = (d, o) => new DataView(d.buffer, d.byteOffset + o, 4).getUint32(0, true);
|
|
11
|
+
const short = (a) => `${a.slice(0, 4)}…${a.slice(-4)}`;
|
|
9
12
|
function isVersioned(tx) {
|
|
10
13
|
return 'version' in tx;
|
|
11
14
|
}
|
|
@@ -32,38 +35,60 @@ export async function instructionsOf(tx, connection) {
|
|
|
32
35
|
ixs: msg.compiledInstructions.map((ix) => ({ programId: k(ix.programIdIndex), accounts: ix.accountKeyIndexes.map(k), data: ix.data })),
|
|
33
36
|
};
|
|
34
37
|
}
|
|
35
|
-
/** Decode
|
|
36
|
-
export async function inspectTransaction(tx, connection) {
|
|
38
|
+
/** Decode what leaves `wallet` (default: the fee payer). Token destinations are resolved to their owner via RPC when needed. */
|
|
39
|
+
export async function inspectTransaction(tx, connection, opts = {}) {
|
|
37
40
|
const { payer, ixs } = await instructionsOf(tx, connection);
|
|
41
|
+
const wallet = opts.wallet ?? payer;
|
|
38
42
|
const outflows = [];
|
|
43
|
+
const risky = [];
|
|
39
44
|
const notes = [];
|
|
40
45
|
const programs = new Set();
|
|
41
|
-
// token accounts created in this transaction: ata -> {
|
|
46
|
+
// token accounts created in this transaction: ata -> { owner, mint } (Create = 0 / empty data, CreateIdempotent = 1)
|
|
42
47
|
const created = new Map();
|
|
43
48
|
for (const ix of ixs)
|
|
44
|
-
if (ix.programId === ATA && ix.accounts.length >= 4)
|
|
49
|
+
if (ix.programId === ATA && (ix.data.length === 0 || ix.data[0] <= 1) && ix.accounts.length >= 4)
|
|
45
50
|
created.set(ix.accounts[1], { owner: ix.accounts[2], mint: ix.accounts[3] });
|
|
46
51
|
const pendingTokens = [];
|
|
52
|
+
const sol = (from, to, lamports, instruction) => outflows.push({ kind: 'sol', from, to, toAccount: to, amount: lamports / 1e9, instruction });
|
|
47
53
|
for (const ix of ixs) {
|
|
48
54
|
programs.add(ix.programId);
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
55
|
+
const a = ix.accounts, d = ix.data;
|
|
56
|
+
if (ix.programId === SYSTEM && d.length >= 4) {
|
|
57
|
+
const op = u32(d, 0);
|
|
58
|
+
if (op === 0 && d.length >= 12)
|
|
59
|
+
sol(a[0], a[1], u64(d, 4), 'system.createAccount');
|
|
60
|
+
else if (op === 2 && d.length >= 12)
|
|
61
|
+
sol(a[0], a[1], u64(d, 4), 'system.transfer');
|
|
62
|
+
else if (op === 11 && d.length >= 12)
|
|
63
|
+
sol(a[1], a[2], u64(d, 4), 'system.transferWithSeed');
|
|
64
|
+
else if (op === 3 && d.length >= 44) {
|
|
65
|
+
const seedLen = u64(d, 36);
|
|
66
|
+
if (d.length >= 44 + seedLen + 8)
|
|
67
|
+
sol(a[0], a[1], u64(d, 44 + seedLen), 'system.createAccountWithSeed');
|
|
68
|
+
}
|
|
69
|
+
else if (op === 5 && d.length >= 12)
|
|
70
|
+
sol(a[4], a[1], u64(d, 4), 'system.withdrawNonceAccount');
|
|
71
|
+
else if ((op === 1 || op === 10) && a[0] === wallet)
|
|
72
|
+
risky.push({ instruction: 'system.assign', message: `Reassigns the wallet ${short(wallet)} to another program` });
|
|
57
73
|
}
|
|
58
|
-
else if ((ix.programId === TOKEN || ix.programId === TOKEN22) &&
|
|
59
|
-
const op =
|
|
60
|
-
if (op === 3)
|
|
61
|
-
pendingTokens.push({ account:
|
|
62
|
-
else if (op === 12 &&
|
|
63
|
-
pendingTokens.push({ account:
|
|
74
|
+
else if ((ix.programId === TOKEN || ix.programId === TOKEN22) && d.length >= 1) {
|
|
75
|
+
const op = d[0];
|
|
76
|
+
if (op === 3 && d.length >= 9)
|
|
77
|
+
pendingTokens.push({ account: a[1], raw: u64(d, 1), authority: a[2], instruction: 'token.transfer' });
|
|
78
|
+
else if (op === 12 && d.length >= 10)
|
|
79
|
+
pendingTokens.push({ account: a[2], raw: u64(d, 1), decimals: d[9], mint: a[1], authority: a[3], instruction: 'token.transferChecked' });
|
|
80
|
+
else if (op === 26 && d[1] === 1 && d.length >= 11)
|
|
81
|
+
pendingTokens.push({ account: a[2], raw: u64(d, 2), decimals: d[10], mint: a[1], authority: a[3], instruction: 'token.transferCheckedWithFee' });
|
|
82
|
+
else if ((op === 4 && a[2] === wallet) || (op === 13 && a[3] === wallet))
|
|
83
|
+
risky.push({ instruction: 'token.approve', message: `Lets ${short(op === 4 ? a[1] : a[2])} spend tokens from ${short(a[0])}` });
|
|
84
|
+
else if (op === 6 && a[1] === wallet)
|
|
85
|
+
risky.push({ instruction: 'token.setAuthority', message: `Changes an authority on ${short(a[0])}, which the wallet controls` });
|
|
86
|
+
else if (op === 9 && a[2] === wallet && a[1] !== wallet)
|
|
87
|
+
risky.push({ instruction: 'token.closeAccount', message: `Closes ${short(a[0])} and sends its lamports (all wrapped SOL) to ${short(a[1])}` });
|
|
64
88
|
}
|
|
65
89
|
}
|
|
66
90
|
// resolve token destinations to owners (and mint/decimals for plain `transfer`)
|
|
91
|
+
const { PublicKey: PK } = connection ? await import('@solana/web3.js') : { PublicKey: undefined };
|
|
67
92
|
for (const t of pendingTokens) {
|
|
68
93
|
let owner, mint = t.mint, decimals = t.decimals;
|
|
69
94
|
const c = created.get(t.account);
|
|
@@ -71,26 +96,30 @@ export async function inspectTransaction(tx, connection) {
|
|
|
71
96
|
owner = c.owner;
|
|
72
97
|
mint ??= c.mint;
|
|
73
98
|
}
|
|
74
|
-
if ((!owner || decimals === undefined) && connection) {
|
|
75
|
-
const info = await connection.getParsedAccountInfo(new (
|
|
99
|
+
if ((!owner || decimals === undefined) && connection && PK) {
|
|
100
|
+
const info = await connection.getParsedAccountInfo(new PK(t.account));
|
|
76
101
|
const parsed = info.value?.data?.parsed?.info;
|
|
77
102
|
owner ??= parsed?.owner;
|
|
78
103
|
mint ??= parsed?.mint;
|
|
79
104
|
decimals ??= parsed?.tokenAmount?.decimals;
|
|
80
105
|
if (decimals === undefined && mint) {
|
|
81
|
-
const m = await connection.getParsedAccountInfo(new (
|
|
106
|
+
const m = await connection.getParsedAccountInfo(new PK(mint));
|
|
82
107
|
decimals = m.value?.data?.parsed?.info?.decimals;
|
|
83
108
|
}
|
|
84
109
|
}
|
|
85
110
|
if (!owner)
|
|
86
111
|
notes.push(`could not resolve the owner of token account ${t.account}; scored as-is`);
|
|
87
|
-
outflows.push({ kind: 'token', to: owner ?? t.account, toAccount: t.account, amount: decimals === undefined ? t.raw : t.raw / 10 ** decimals, mint, instruction: t.instruction });
|
|
112
|
+
outflows.push({ kind: 'token', from: t.authority, to: owner ?? t.account, toAccount: t.account, amount: decimals === undefined ? t.raw : t.raw / 10 ** decimals, mint, decimalsKnown: decimals !== undefined, instruction: t.instruction });
|
|
88
113
|
}
|
|
89
114
|
const all = [...programs];
|
|
90
|
-
const unknownPrograms = all.filter((p) => !
|
|
91
|
-
|
|
115
|
+
const unknownPrograms = all.filter((p) => !INERT.has(p) && !KNOWN_PROGRAMS[p]);
|
|
116
|
+
const valuePrograms = all.filter((p) => !INERT.has(p) && KNOWN_PROGRAMS[p]).map((p) => KNOWN_PROGRAMS[p]);
|
|
92
117
|
for (const p of all)
|
|
93
|
-
if (!
|
|
118
|
+
if (!INERT.has(p))
|
|
94
119
|
notes.push(`calls ${KNOWN_PROGRAMS[p] ?? p}: value moved inside this program is not decoded`);
|
|
95
|
-
|
|
120
|
+
// only value that leaves the guarded wallet counts; transfers between other parties in the same transaction do not
|
|
121
|
+
const mine = outflows.filter((o) => o.from === wallet && o.to !== wallet && o.amount > 0);
|
|
122
|
+
if (mine.length < outflows.filter((o) => o.amount > 0).length)
|
|
123
|
+
notes.push('some transfers in this transaction are not from the guarded wallet and were not counted');
|
|
124
|
+
return { payer, wallet, outflows: mine, programs: all, unknownPrograms, valuePrograms, risky, notes };
|
|
96
125
|
}
|
package/dist/mcp.d.ts
ADDED
package/dist/mcp.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// MCP server over stdio (newline-delimited JSON-RPC 2.0), no extra dependencies.
|
|
2
|
+
// Start with `npx -y veralayer mcp`; add to Claude Desktop / Cursor as a stdio server.
|
|
3
|
+
// Tools: vera_check (score an address) and vera_guard_payment (full guard decision for a planned SOL payment).
|
|
4
|
+
import { createInterface } from 'node:readline';
|
|
5
|
+
import { Vera } from './client.js';
|
|
6
|
+
import { VeraGuard } from './guard.js';
|
|
7
|
+
const VERSION = '0.2.0';
|
|
8
|
+
const PROTOCOLS = ['2025-06-18', '2025-03-26', '2024-11-05'];
|
|
9
|
+
const TOOLS = [
|
|
10
|
+
{
|
|
11
|
+
name: 'vera_check',
|
|
12
|
+
title: 'Vera trust check',
|
|
13
|
+
description: 'Score a Solana address before paying it. Returns a 0-100 trust score, a tier (high/medium/low/unknown), the reasons, and whether Vera\'s signed result verified. A score is a risk signal, not a guarantee.',
|
|
14
|
+
inputSchema: { type: 'object', properties: { address: { type: 'string', description: 'Solana address (base58)' } }, required: ['address'] },
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
name: 'vera_guard_payment',
|
|
18
|
+
title: 'Vera payment guard',
|
|
19
|
+
description: 'Decide whether a planned SOL payment should go ahead under a spending policy. Returns allow, review or block with the rule that fired. Call this before sending funds.',
|
|
20
|
+
inputSchema: {
|
|
21
|
+
type: 'object',
|
|
22
|
+
properties: {
|
|
23
|
+
to: { type: 'string', description: 'Recipient Solana address' },
|
|
24
|
+
amount_sol: { type: 'number', description: 'Amount in SOL' },
|
|
25
|
+
min_tier: { type: 'string', enum: ['high', 'medium', 'low'], description: 'Minimum recipient tier (default medium)' },
|
|
26
|
+
max_sol_per_tx: { type: 'number', description: 'Optional cap for this payment' },
|
|
27
|
+
},
|
|
28
|
+
required: ['to', 'amount_sol'],
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
];
|
|
32
|
+
export async function runMcp(opts = {}) {
|
|
33
|
+
const vera = new Vera({ ...opts, issuer: process.env.VERA_ISSUER });
|
|
34
|
+
const send = (msg) => process.stdout.write(JSON.stringify(msg) + '\n');
|
|
35
|
+
const text = (t, structured, isError = false) => ({ content: [{ type: 'text', text: t }], ...(structured ? { structuredContent: structured } : {}), ...(isError ? { isError } : {}) });
|
|
36
|
+
async function call(name, args) {
|
|
37
|
+
try {
|
|
38
|
+
if (name === 'vera_check') {
|
|
39
|
+
const address = String(args.address ?? '').trim();
|
|
40
|
+
const r = await vera.check(address);
|
|
41
|
+
const v = await vera.verify(r);
|
|
42
|
+
const a = r.attestation;
|
|
43
|
+
const lines = [
|
|
44
|
+
`${a.score ?? 'n/a'}/100 · ${a.tier.toUpperCase()} · ${address}`,
|
|
45
|
+
...(r.summary ? [r.summary] : []),
|
|
46
|
+
...a.reasons.map((x) => `- [${x.kind}] ${x.text}`),
|
|
47
|
+
`Signature ${v.valid ? 'verified' : 'INVALID'} · attestation ${r.id}`,
|
|
48
|
+
];
|
|
49
|
+
return text(lines.join('\n'), { score: a.score, tier: a.tier, reasons: a.reasons, verified: v.valid, attestation_id: r.id });
|
|
50
|
+
}
|
|
51
|
+
if (name === 'vera_guard_payment') {
|
|
52
|
+
const policy = {
|
|
53
|
+
...(args.min_tier ? { minTier: args.min_tier } : {}),
|
|
54
|
+
...(typeof args.max_sol_per_tx === 'number' ? { maxSolPerTx: args.max_sol_per_tx } : {}),
|
|
55
|
+
};
|
|
56
|
+
const amount = Number(args.amount_sol);
|
|
57
|
+
if (!Number.isFinite(amount) || amount < 0)
|
|
58
|
+
return text(`Invalid amount_sol: ${String(args.amount_sol)}`, undefined, true);
|
|
59
|
+
const d = await new VeraGuard({ vera, policy }).evaluatePayment({ to: String(args.to), amountSol: amount });
|
|
60
|
+
const lines = [`${d.action.toUpperCase()}: ${args.amount_sol} SOL to ${args.to}`, ...d.findings.map((f) => `- ${f.action}: ${f.message}`)];
|
|
61
|
+
if (!d.findings.length)
|
|
62
|
+
lines.push('- no policy rule fired');
|
|
63
|
+
return text(lines.join('\n'), { action: d.action, findings: d.findings });
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
catch (e) {
|
|
68
|
+
return text(`Vera failed: ${e.message}`, undefined, true);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const rl = createInterface({ input: process.stdin });
|
|
72
|
+
rl.on('line', async (line) => {
|
|
73
|
+
if (!line.trim())
|
|
74
|
+
return;
|
|
75
|
+
let msg;
|
|
76
|
+
try {
|
|
77
|
+
msg = JSON.parse(line);
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return send({ jsonrpc: '2.0', id: null, error: { code: -32700, message: 'Parse error' } });
|
|
81
|
+
}
|
|
82
|
+
const { id, method, params } = msg;
|
|
83
|
+
if (id === undefined)
|
|
84
|
+
return; // notifications (e.g. notifications/initialized) need no reply
|
|
85
|
+
if (typeof method !== 'string')
|
|
86
|
+
return send({ jsonrpc: '2.0', id, error: { code: -32600, message: 'Invalid request' } });
|
|
87
|
+
const reply = (result) => send({ jsonrpc: '2.0', id, result });
|
|
88
|
+
switch (method) {
|
|
89
|
+
case 'initialize':
|
|
90
|
+
return reply({ protocolVersion: PROTOCOLS.includes(params?.protocolVersion) ? params?.protocolVersion : PROTOCOLS[0], capabilities: { tools: {} }, serverInfo: { name: 'veralayer', version: VERSION } });
|
|
91
|
+
case 'ping':
|
|
92
|
+
return reply({});
|
|
93
|
+
case 'tools/list':
|
|
94
|
+
return reply({ tools: TOOLS });
|
|
95
|
+
case 'tools/call': {
|
|
96
|
+
const res = await call(String(params?.name), params?.arguments ?? {});
|
|
97
|
+
return res ? reply(res) : send({ jsonrpc: '2.0', id, error: { code: -32602, message: `Unknown tool: ${String(params?.name)}` } });
|
|
98
|
+
}
|
|
99
|
+
default:
|
|
100
|
+
return send({ jsonrpc: '2.0', id, error: { code: -32601, message: `Method not found: ${method}` } });
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
}
|
package/dist/policy.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ export type Action = 'allow' | 'review' | 'block';
|
|
|
3
3
|
export type Policy = {
|
|
4
4
|
/** Recipients below this tier are blocked. Default 'medium'. */
|
|
5
5
|
minTier?: Exclude<Tier, 'unknown'>;
|
|
6
|
-
/** Max SOL one recipient may receive in one transaction, per tier. Default high: no cap, medium: 5, low: 0
|
|
6
|
+
/** Max SOL one recipient may receive in one transaction, per tier. Default high: no cap, medium: 5, low: 0. Unknown recipients are governed by onUnknown unless you set a cap here. */
|
|
7
7
|
tierLimitsSol?: Partial<Record<Tier, number>>;
|
|
8
8
|
/** Max total SOL leaving the wallet in one transaction. */
|
|
9
9
|
maxSolPerTx?: number;
|
|
@@ -15,25 +15,48 @@ export type Policy = {
|
|
|
15
15
|
allow?: string[];
|
|
16
16
|
/** Addresses that are always blocked. */
|
|
17
17
|
deny?: string[];
|
|
18
|
-
/**
|
|
19
|
-
onUnknown?:
|
|
20
|
-
/**
|
|
18
|
+
/** Recipients Vera cannot score (no history). Default 'review'. */
|
|
19
|
+
onUnknown?: Action;
|
|
20
|
+
/** The check failed, its signature did not verify, or it was for another address. Default 'block'. */
|
|
21
21
|
onCheckError?: Exclude<Action, 'allow'>;
|
|
22
22
|
/** Transactions that call programs outside the well-known list. Default 'review'. */
|
|
23
23
|
unknownPrograms?: Action;
|
|
24
|
+
/** Well-known programs that can move value the guard does not decode (swaps, lending). Default 'review'. */
|
|
25
|
+
knownPrograms?: Action;
|
|
26
|
+
/** Instructions that hand over control of the wallet or its tokens (approve, setAuthority, closeAccount to someone else, assign). Default 'block'. */
|
|
27
|
+
riskyInstructions?: Action;
|
|
24
28
|
};
|
|
25
|
-
export declare const DEFAULT_POLICY:
|
|
26
|
-
|
|
29
|
+
export declare const DEFAULT_POLICY: {
|
|
30
|
+
minTier: "medium";
|
|
31
|
+
tierLimitsSol: Partial<Record<Tier, number>>;
|
|
32
|
+
allow: string[];
|
|
33
|
+
deny: string[];
|
|
34
|
+
onUnknown: Action;
|
|
35
|
+
onCheckError: Exclude<Action, "allow">;
|
|
36
|
+
unknownPrograms: Action;
|
|
37
|
+
knownPrograms: Action;
|
|
38
|
+
riskyInstructions: Action;
|
|
39
|
+
};
|
|
40
|
+
/** One movement of value out of the guarded wallet. */
|
|
27
41
|
export type Outflow = {
|
|
28
42
|
kind: 'sol' | 'token';
|
|
29
43
|
/** The wallet that ends up owning the funds (token accounts are resolved to their owner). */
|
|
30
44
|
to: string;
|
|
31
45
|
/** The account that is credited: same as `to` for SOL, the token account for SPL. */
|
|
32
46
|
toAccount: string;
|
|
33
|
-
/** SOL for kind 'sol'; UI units for tokens. */
|
|
47
|
+
/** SOL for kind 'sol'; UI units for tokens (raw units when decimals are unknown). */
|
|
34
48
|
amount: number;
|
|
35
49
|
mint?: string;
|
|
50
|
+
/** false when token decimals could not be resolved, so `amount` is in raw units. */
|
|
51
|
+
decimalsKnown?: boolean;
|
|
52
|
+
/** The wallet the value leaves (the signer of the transfer). */
|
|
53
|
+
from?: string;
|
|
54
|
+
instruction: string;
|
|
55
|
+
};
|
|
56
|
+
/** An instruction that changes who controls the wallet or its tokens. */
|
|
57
|
+
export type RiskyInstruction = {
|
|
36
58
|
instruction: string;
|
|
59
|
+
message: string;
|
|
37
60
|
};
|
|
38
61
|
/** What the guard learned about one recipient. */
|
|
39
62
|
export type RecipientCheck = {
|
|
@@ -67,10 +90,15 @@ export type Decision = {
|
|
|
67
90
|
totalSol: number;
|
|
68
91
|
unknownPrograms: string[];
|
|
69
92
|
};
|
|
93
|
+
/** Throws on a policy that would silently weaken checks (typos in tiers or actions, non-numeric limits). */
|
|
94
|
+
export declare function validatePolicy(p?: Policy): void;
|
|
70
95
|
export declare function evaluatePolicy(input: {
|
|
71
96
|
outflows: Outflow[];
|
|
72
97
|
recipients: RecipientCheck[];
|
|
73
98
|
unknownPrograms?: string[];
|
|
99
|
+
/** names of well-known programs that may move value the guard does not decode */
|
|
100
|
+
valuePrograms?: string[];
|
|
101
|
+
risky?: RiskyInstruction[];
|
|
74
102
|
spentTodaySol?: number;
|
|
75
103
|
policy?: Policy;
|
|
76
104
|
}): Decision;
|
package/dist/policy.js
CHANGED
|
@@ -1,70 +1,112 @@
|
|
|
1
1
|
export const DEFAULT_POLICY = {
|
|
2
2
|
minTier: 'medium',
|
|
3
|
-
tierLimitsSol: { high: Infinity, medium: 5, low: 0
|
|
3
|
+
tierLimitsSol: { high: Infinity, medium: 5, low: 0 },
|
|
4
4
|
allow: [],
|
|
5
5
|
deny: [],
|
|
6
6
|
onUnknown: 'review',
|
|
7
7
|
onCheckError: 'block',
|
|
8
8
|
unknownPrograms: 'review',
|
|
9
|
+
knownPrograms: 'review',
|
|
10
|
+
riskyInstructions: 'block',
|
|
9
11
|
};
|
|
10
12
|
const RANK = { unknown: 0, low: 1, medium: 2, high: 3 };
|
|
13
|
+
const ACTIONS = ['allow', 'review', 'block'];
|
|
11
14
|
const worst = (a, b) => (a === 'block' || b === 'block' ? 'block' : a === 'review' || b === 'review' ? 'review' : 'allow');
|
|
12
15
|
const fmt = (n) => (Number.isInteger(n) ? String(n) : n.toFixed(4).replace(/0+$/, ''));
|
|
16
|
+
const short = (a) => `${a.slice(0, 4)}…${a.slice(-4)}`;
|
|
17
|
+
/** Throws on a policy that would silently weaken checks (typos in tiers or actions, non-numeric limits). */
|
|
18
|
+
export function validatePolicy(p = {}) {
|
|
19
|
+
if (p.minTier !== undefined && !['high', 'medium', 'low'].includes(p.minTier))
|
|
20
|
+
throw new Error(`invalid minTier: ${String(p.minTier)}`);
|
|
21
|
+
for (const k of ['onUnknown', 'onCheckError', 'unknownPrograms', 'knownPrograms', 'riskyInstructions']) {
|
|
22
|
+
if (p[k] !== undefined && !ACTIONS.includes(p[k]))
|
|
23
|
+
throw new Error(`invalid ${k}: ${String(p[k])}`);
|
|
24
|
+
}
|
|
25
|
+
if (p.onCheckError === 'allow')
|
|
26
|
+
throw new Error('onCheckError cannot be allow');
|
|
27
|
+
const nums = [['maxSolPerTx', p.maxSolPerTx], ['dailyLimitSol', p.dailyLimitSol],
|
|
28
|
+
...Object.entries(p.tierLimitsSol ?? {}).map(([k, v]) => [`tierLimitsSol.${k}`, v]),
|
|
29
|
+
...Object.entries(p.tokenLimits ?? {}).map(([k, v]) => [`tokenLimits.${k}`, v])];
|
|
30
|
+
for (const [k, v] of nums)
|
|
31
|
+
if (v !== undefined && (typeof v !== 'number' || Number.isNaN(v) || v < 0))
|
|
32
|
+
throw new Error(`invalid ${k}: ${String(v)}`);
|
|
33
|
+
for (const k of Object.keys(p.tierLimitsSol ?? {}))
|
|
34
|
+
if (!(k in RANK))
|
|
35
|
+
throw new Error(`invalid tier in tierLimitsSol: ${k}`);
|
|
36
|
+
}
|
|
13
37
|
export function evaluatePolicy(input) {
|
|
38
|
+
validatePolicy(input.policy);
|
|
14
39
|
const p = { ...DEFAULT_POLICY, ...input.policy, tierLimitsSol: { ...DEFAULT_POLICY.tierLimitsSol, ...input.policy?.tierLimitsSol } };
|
|
15
40
|
const allow = new Set(p.allow), deny = new Set(p.deny);
|
|
16
41
|
const findings = [];
|
|
17
42
|
const add = (action, code, message, address) => findings.push({ action, code, message, ...(address ? { address } : {}) });
|
|
18
|
-
|
|
19
|
-
const
|
|
43
|
+
// malformed amounts never pass and never count toward limits
|
|
44
|
+
const good = (o) => typeof o.amount === 'number' && Number.isFinite(o.amount) && o.amount >= 0;
|
|
20
45
|
for (const o of input.outflows)
|
|
46
|
+
if (!good(o))
|
|
47
|
+
add('block', 'bad_amount', `Invalid amount ${String(o.amount)} to ${short(String(o.to))}`, o.to);
|
|
48
|
+
const outflows = input.outflows.filter(good);
|
|
49
|
+
const totalSol = outflows.filter((o) => o.kind === 'sol').reduce((s, o) => s + o.amount, 0);
|
|
50
|
+
const solTo = new Map();
|
|
51
|
+
for (const o of outflows)
|
|
21
52
|
if (o.kind === 'sol')
|
|
22
53
|
solTo.set(o.to, (solTo.get(o.to) || 0) + o.amount);
|
|
23
54
|
const byAddr = new Map(input.recipients.map((r) => [r.address, r]));
|
|
24
|
-
for (const to of new Set(
|
|
25
|
-
const
|
|
55
|
+
for (const to of new Set(outflows.map((o) => o.to))) {
|
|
56
|
+
const s = short(to);
|
|
26
57
|
if (deny.has(to)) {
|
|
27
|
-
add('block', 'denylisted', `${
|
|
58
|
+
add('block', 'denylisted', `${s} is on your deny list`, to);
|
|
28
59
|
continue;
|
|
29
60
|
}
|
|
30
61
|
if (allow.has(to))
|
|
31
62
|
continue;
|
|
32
63
|
const r = byAddr.get(to);
|
|
33
64
|
if (!r || !r.ok) {
|
|
34
|
-
add(p.onCheckError, 'check_failed', `Could not check ${
|
|
65
|
+
add(p.onCheckError, 'check_failed', `Could not check ${s}${r && !r.ok ? `: ${r.error}` : ''}`, to);
|
|
35
66
|
continue;
|
|
36
67
|
}
|
|
37
|
-
if (!r.
|
|
38
|
-
add(p.onCheckError, '
|
|
68
|
+
if (!(r.tier in RANK)) {
|
|
69
|
+
add(p.onCheckError, 'bad_tier', `Vera returned an unexpected tier for ${s}`, to);
|
|
39
70
|
continue;
|
|
40
71
|
}
|
|
41
|
-
if (r.
|
|
42
|
-
add(
|
|
72
|
+
if (!r.verified) {
|
|
73
|
+
add(p.onCheckError, 'bad_signature', `Vera's signature for ${s} did not verify`, to);
|
|
74
|
+
continue;
|
|
43
75
|
}
|
|
76
|
+
if (r.expired)
|
|
77
|
+
add('review', 'stale_check', `Vera's result for ${s} has expired`, to);
|
|
44
78
|
if (r.tier === 'unknown') {
|
|
45
|
-
add(p.onUnknown, 'unknown_recipient', `${
|
|
79
|
+
add(p.onUnknown, 'unknown_recipient', `${s} has no history Vera can score`, to);
|
|
46
80
|
}
|
|
47
81
|
else if (RANK[r.tier] < RANK[p.minTier]) {
|
|
48
|
-
add('block', 'tier_below_minimum', `${
|
|
82
|
+
add('block', 'tier_below_minimum', `${s} is ${r.tier} trust (${r.score}/100), below your minimum of ${p.minTier}: ${r.reasons[0]?.text ?? ''}`.trim(), to);
|
|
49
83
|
}
|
|
50
84
|
const cap = p.tierLimitsSol[r.tier] ?? Infinity;
|
|
51
85
|
const sent = solTo.get(to) || 0;
|
|
52
|
-
if (sent > cap
|
|
53
|
-
add(cap === 0 ? 'block' : 'review', 'tier_limit', `${fmt(sent)} SOL to ${
|
|
54
|
-
}
|
|
86
|
+
if (sent > cap)
|
|
87
|
+
add(cap === 0 ? 'block' : 'review', 'tier_limit', `${fmt(sent)} SOL to ${s} exceeds the ${fmt(cap)} SOL limit for ${r.tier} trust`, to);
|
|
55
88
|
}
|
|
56
|
-
for (const o of
|
|
57
|
-
if (o.kind !== 'token'
|
|
89
|
+
for (const o of outflows) {
|
|
90
|
+
if (o.kind !== 'token')
|
|
58
91
|
continue;
|
|
59
|
-
if (o.
|
|
92
|
+
if (o.decimalsKnown === false) {
|
|
93
|
+
add('review', 'unknown_decimals', `Could not read token decimals for ${short(o.toAccount)}; amount not compared to limits`, o.to);
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (o.mint && p.tokenLimits && o.mint in p.tokenLimits && o.amount > p.tokenLimits[o.mint])
|
|
60
97
|
add('review', 'token_limit', `${fmt(o.amount)} of ${o.mint.slice(0, 4)}… exceeds your ${fmt(p.tokenLimits[o.mint])} per-transaction cap`, o.to);
|
|
61
98
|
}
|
|
62
99
|
if (p.maxSolPerTx !== undefined && totalSol > p.maxSolPerTx)
|
|
63
100
|
add('block', 'tx_limit', `${fmt(totalSol)} SOL in one transaction exceeds your ${fmt(p.maxSolPerTx)} SOL cap`);
|
|
64
101
|
if (p.dailyLimitSol !== undefined && (input.spentTodaySol ?? 0) + totalSol > p.dailyLimitSol)
|
|
65
102
|
add('block', 'daily_limit', `This would bring today's spend to ${fmt((input.spentTodaySol ?? 0) + totalSol)} SOL, over your ${fmt(p.dailyLimitSol)} SOL daily limit`);
|
|
103
|
+
for (const r of input.risky ?? [])
|
|
104
|
+
if (p.riskyInstructions !== 'allow')
|
|
105
|
+
add(p.riskyInstructions, 'risky_instruction', r.message);
|
|
66
106
|
if (input.unknownPrograms?.length && p.unknownPrograms !== 'allow')
|
|
67
107
|
add(p.unknownPrograms, 'unknown_program', `Calls ${input.unknownPrograms.length} program${input.unknownPrograms.length > 1 ? 's' : ''} outside the well-known list: ${input.unknownPrograms.map((x) => x.slice(0, 4) + '…').join(', ')}`);
|
|
108
|
+
if (input.valuePrograms?.length && p.knownPrograms !== 'allow')
|
|
109
|
+
add(p.knownPrograms, 'opaque_value', `Value may move inside ${input.valuePrograms.join(', ')}, which the guard does not decode`);
|
|
68
110
|
const action = findings.reduce((a, f) => worst(a, f.action), 'allow');
|
|
69
|
-
return { action, findings, outflows
|
|
111
|
+
return { action, findings, outflows, recipients: input.recipients, totalSol, unknownPrograms: input.unknownPrograms ?? [] };
|
|
70
112
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "veralayer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Payment guard for AI agents on Solana: inspects every outgoing transaction, scores each recipient with Vera, and enforces your spending policy before anything is signed.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -17,7 +17,12 @@
|
|
|
17
17
|
"./client": {
|
|
18
18
|
"types": "./dist/client.d.ts",
|
|
19
19
|
"import": "./dist/client.js"
|
|
20
|
-
}
|
|
20
|
+
},
|
|
21
|
+
"./agent-kit": {
|
|
22
|
+
"types": "./dist/agent-kit.d.ts",
|
|
23
|
+
"import": "./dist/agent-kit.js"
|
|
24
|
+
},
|
|
25
|
+
"./package.json": "./package.json"
|
|
21
26
|
},
|
|
22
27
|
"bin": {
|
|
23
28
|
"veralayer": "dist/cli.js"
|
|
@@ -27,10 +32,13 @@
|
|
|
27
32
|
"README.md",
|
|
28
33
|
"LICENSE"
|
|
29
34
|
],
|
|
30
|
-
"sideEffects":
|
|
35
|
+
"sideEffects": [
|
|
36
|
+
"./dist/cli.js",
|
|
37
|
+
"./dist/mcp.js"
|
|
38
|
+
],
|
|
31
39
|
"scripts": {
|
|
32
40
|
"build": "tsc",
|
|
33
|
-
"test": "node --env-file-if-exists=../../../../checker/.env test/run.mjs",
|
|
41
|
+
"test": "node --env-file-if-exists=../../../../checker/.env test/run.mjs && node --env-file-if-exists=../../../../checker/.env test/security.mjs",
|
|
34
42
|
"prepublishOnly": "tsc && node test/smoke.mjs"
|
|
35
43
|
},
|
|
36
44
|
"keywords": [
|
|
@@ -41,19 +49,26 @@
|
|
|
41
49
|
"risk",
|
|
42
50
|
"guard",
|
|
43
51
|
"mcp",
|
|
44
|
-
"solana-agent-kit"
|
|
52
|
+
"solana-agent-kit",
|
|
53
|
+
"model-context-protocol",
|
|
54
|
+
"claude",
|
|
55
|
+
"cursor"
|
|
45
56
|
],
|
|
46
|
-
"homepage": "https://
|
|
57
|
+
"homepage": "https://veraagent.io",
|
|
47
58
|
"license": "MIT",
|
|
48
59
|
"engines": {
|
|
49
|
-
"node": ">=
|
|
60
|
+
"node": ">=20.19"
|
|
50
61
|
},
|
|
51
62
|
"peerDependencies": {
|
|
52
|
-
"@solana/web3.js": "^1.95.0"
|
|
63
|
+
"@solana/web3.js": "^1.95.0",
|
|
64
|
+
"zod": ">=3.22"
|
|
53
65
|
},
|
|
54
66
|
"peerDependenciesMeta": {
|
|
55
67
|
"@solana/web3.js": {
|
|
56
68
|
"optional": true
|
|
69
|
+
},
|
|
70
|
+
"zod": {
|
|
71
|
+
"optional": true
|
|
57
72
|
}
|
|
58
73
|
},
|
|
59
74
|
"dependencies": {
|
|
@@ -63,6 +78,7 @@
|
|
|
63
78
|
"devDependencies": {
|
|
64
79
|
"@solana/web3.js": "^1.99.0",
|
|
65
80
|
"@types/node": "^26.6.6",
|
|
66
|
-
"typescript": "^5.9.3"
|
|
81
|
+
"typescript": "^5.9.3",
|
|
82
|
+
"zod": "^4.6.7"
|
|
67
83
|
}
|
|
68
84
|
}
|