@saw-protocol/policy 0.1.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 ADDED
@@ -0,0 +1,65 @@
1
+ # @saw-protocol/policy
2
+
3
+ The standard execution Policy Engine for the SAWP Protocol.
4
+
5
+ This module acts as the protocol's firewall. It implements `IPolicyEngine` and ensures agents never exceed financial constraints, communicate with untrusted programs, or violate delegated bounds before constructing blockchain transactions.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install @saw-protocol/policy
11
+ ```
12
+
13
+ ## Example Usage
14
+
15
+ ```typescript
16
+ import { CorePolicyEngine } from "@saw-protocol/policy";
17
+ import { PublicKey } from "@solana/web3.js";
18
+ import { TransactionIntent } from "@saw-protocol/core";
19
+ import { v4 as uuidv4 } from "uuid";
20
+
21
+ const mockStorage: IStorageProvider<any> = {
22
+ get: async (key: string) => null,
23
+ set: async (key: string, value: any) => {},
24
+ delete: async (key: string) => {},
25
+ };
26
+
27
+ const engine = new CorePolicyEngine(mockStorage);
28
+ const walletPubkey = new PublicKey("..."); // Target wallet address
29
+
30
+ // Add foundational safety boundaries
31
+ await engine.addRule(walletPubkey, {
32
+ type: "SpendLimit",
33
+ maxSOLPerTx: 0.1, // Never allow a transaction intent above 0.1 SOL
34
+ maxSOLPerDay: 1.0,
35
+ });
36
+
37
+ await engine.addRule(walletPubkey, {
38
+ type: "ProgramAllowList",
39
+ allowedProgramIds: [new PublicKey("JUP6LkbZbjS1jKKwapdH67yIeU1B...")], // Only Jupyter Swaps
40
+ });
41
+
42
+ // A malicious Agent tries to request a transfer of 5 SOL to an unknown program
43
+ const badIntent: TransactionIntent = {
44
+ id: uuidv4(),
45
+ agentId: "did:sol:someAgent",
46
+ walletAddress: walletPubkey,
47
+ action: {
48
+ type: "transfer",
49
+ estimatedValue: 5.0, // Instantly stopped by SpendLimit
50
+ params: {},
51
+ },
52
+ reasoning: "Attempting to drain funds",
53
+ signature: "...",
54
+ timestamp: Date.now(),
55
+ };
56
+
57
+ // Evaluate the structural intent. Does NOT communicate with the chain.
58
+ const evaluation = await engine.evaluate(badIntent, mockWalletObj, {
59
+ network: "devnet",
60
+ currentBalance: 10,
61
+ });
62
+
63
+ console.log(evaluation.allowed); // false
64
+ console.log(evaluation.reason); // "Policy Denied: Estimated value 5 exceeds per-tx limit of 0.1"
65
+ ```
@@ -0,0 +1,26 @@
1
+ import { IPolicyEngine, EvaluationContext, EvaluationResult, TransactionIntent, AgentWallet, DelegationToken, PolicyRule, PolicySet, IStorageProvider } from '@saw-protocol/core';
2
+ import { PublicKey } from '@solana/web3.js';
3
+ /**
4
+ * The standard rule-based Policy Engine implementation for the SAWP Protocol.
5
+ * Evaluates intents against a dynamically managed set of strict `PolicySet` rules.
6
+ */
7
+ export declare class CorePolicyEngine implements IPolicyEngine {
8
+ private _store?;
9
+ private _walletPolicies;
10
+ private _rateLimitState;
11
+ private _dailySpendState;
12
+ constructor(store?: IStorageProvider);
13
+ /** Gets or initializes a policy set for a wallet */
14
+ private _getOrCreatePolicySet;
15
+ private _getOrInitDailySpend;
16
+ private _getOrInitRateLimit;
17
+ private _evaluateRule;
18
+ /** Mutates in-memory spend/rate trackers after a successful transaction */
19
+ private _recordSpend;
20
+ evaluate(intent: TransactionIntent, wallet: AgentWallet, context: EvaluationContext): Promise<EvaluationResult>;
21
+ evaluateDelegation(token: DelegationToken, intent: TransactionIntent): Promise<EvaluationResult>;
22
+ addRule(walletAddress: PublicKey, rule: PolicyRule): Promise<void>;
23
+ removeRule(walletAddress: PublicKey, ruleId: string): Promise<void>;
24
+ listRules(walletAddress: PublicKey): Promise<PolicySet>;
25
+ simulate(intent: TransactionIntent, wallet: AgentWallet): Promise<EvaluationResult>;
26
+ }
package/dist/index.js ADDED
@@ -0,0 +1,373 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CorePolicyEngine = void 0;
4
+ const uuid_1 = require("uuid");
5
+ /**
6
+ * The standard rule-based Policy Engine implementation for the SAWP Protocol.
7
+ * Evaluates intents against a dynamically managed set of strict `PolicySet` rules.
8
+ */
9
+ class CorePolicyEngine {
10
+ _store;
11
+ _walletPolicies = new Map();
12
+ // In-memory rate-limit and daily-spend trackers (not persisted for performance)
13
+ _rateLimitState = new Map();
14
+ _dailySpendState = new Map();
15
+ constructor(store) {
16
+ this._store = store;
17
+ }
18
+ /** Gets or initializes a policy set for a wallet */
19
+ async _getOrCreatePolicySet(walletAddress) {
20
+ const key = `policy:${walletAddress.toBase58()}`;
21
+ // Check external store first
22
+ if (this._store) {
23
+ const stored = await this._store.get(key);
24
+ if (stored)
25
+ return stored;
26
+ }
27
+ // Check in-memory fallback
28
+ const memKey = walletAddress.toBase58();
29
+ if (this._walletPolicies.has(memKey)) {
30
+ return this._walletPolicies.get(memKey);
31
+ }
32
+ // Create default — defaultAction 'allow' so wallets work out of the box.
33
+ // Developers add rules to tighten as needed.
34
+ const newPolicy = {
35
+ id: (0, uuid_1.v4)(),
36
+ rules: [],
37
+ defaultAction: 'allow',
38
+ createdAt: Date.now(),
39
+ updatedAt: Date.now()
40
+ };
41
+ if (this._store) {
42
+ await this._store.set(key, newPolicy);
43
+ }
44
+ else {
45
+ this._walletPolicies.set(memKey, newPolicy);
46
+ }
47
+ return newPolicy;
48
+ }
49
+ _getOrInitDailySpend(walletKey) {
50
+ const now = Date.now();
51
+ const startOfDay = new Date();
52
+ startOfDay.setUTCHours(0, 0, 0, 0);
53
+ const dayStart = startOfDay.getTime();
54
+ const existing = this._dailySpendState.get(walletKey);
55
+ if (existing && existing.dayStart === dayStart)
56
+ return existing;
57
+ // New day — reset
58
+ const fresh = { dayStart, totalSOL: 0, perMint: {} };
59
+ this._dailySpendState.set(walletKey, fresh);
60
+ return fresh;
61
+ }
62
+ _getOrInitRateLimit(walletKey) {
63
+ const now = Date.now();
64
+ const existing = this._rateLimitState.get(walletKey);
65
+ if (existing && now - existing.windowStart < 60_000)
66
+ return existing;
67
+ // New minute window
68
+ const fresh = { windowStart: now, count: 0 };
69
+ this._rateLimitState.set(walletKey, fresh);
70
+ return fresh;
71
+ }
72
+ _evaluateRule(rule, intent, wallet, context) {
73
+ const walletKey = wallet.address.toBase58();
74
+ switch (rule.type) {
75
+ case 'SpendLimit': {
76
+ if (intent.action.estimatedValue > rule.maxSOLPerTx) {
77
+ return {
78
+ passed: false,
79
+ reason: `Value ${intent.action.estimatedValue} SOL exceeds per-tx limit of ${rule.maxSOLPerTx} SOL`
80
+ };
81
+ }
82
+ // Enforce daily limit from in-memory tracker
83
+ const daily = this._getOrInitDailySpend(walletKey);
84
+ if (daily.totalSOL + intent.action.estimatedValue > rule.maxSOLPerDay) {
85
+ return {
86
+ passed: false,
87
+ reason: `Daily spend would reach ${(daily.totalSOL + intent.action.estimatedValue).toFixed(4)} SOL, limit is ${rule.maxSOLPerDay} SOL`
88
+ };
89
+ }
90
+ return { passed: true };
91
+ }
92
+ case 'TokenSpendLimit': {
93
+ const mintKey = rule.mint.toBase58();
94
+ const amount = Number(intent.action.params?.amount || 0);
95
+ if (amount > rule.maxPerTx) {
96
+ return {
97
+ passed: false,
98
+ reason: `Token amount ${amount} exceeds per-tx limit of ${rule.maxPerTx} for mint ${mintKey}`
99
+ };
100
+ }
101
+ const daily = this._getOrInitDailySpend(walletKey);
102
+ const mintSpent = daily.perMint[mintKey] || 0;
103
+ if (mintSpent + amount > rule.maxPerDay) {
104
+ return {
105
+ passed: false,
106
+ reason: `Daily token spend would reach ${mintSpent + amount}, limit is ${rule.maxPerDay} for mint ${mintKey}`
107
+ };
108
+ }
109
+ return { passed: true };
110
+ }
111
+ case 'ProgramAllowList':
112
+ if (intent.action.programId &&
113
+ !rule.allowedProgramIds.some(p => p.toBase58() === intent.action.programId?.toBase58())) {
114
+ return {
115
+ passed: false,
116
+ reason: `Program ${intent.action.programId.toBase58()} is not in the ProgramAllowList`
117
+ };
118
+ }
119
+ return { passed: true };
120
+ case 'ProgramDenyList':
121
+ if (intent.action.programId &&
122
+ rule.blockedProgramIds.some(p => p.toBase58() === intent.action.programId?.toBase58())) {
123
+ return {
124
+ passed: false,
125
+ reason: `Program ${intent.action.programId.toBase58()} is blocked by ProgramDenyList`
126
+ };
127
+ }
128
+ return { passed: true };
129
+ case 'Network':
130
+ if (!rule.allowedNetworks.includes(context.network)) {
131
+ return {
132
+ passed: false,
133
+ reason: `Network '${context.network}' is not in the allowed list [${rule.allowedNetworks.join(', ')}]`
134
+ };
135
+ }
136
+ return { passed: true };
137
+ case 'TimeWindow': {
138
+ const [startHour, endHour] = rule.allowedHoursUTC;
139
+ const currentHour = new Date().getUTCHours();
140
+ const inWindow = startHour <= endHour
141
+ ? currentHour >= startHour && currentHour < endHour
142
+ : currentHour >= startHour || currentHour < endHour; // overnight window
143
+ if (!inWindow) {
144
+ return {
145
+ passed: false,
146
+ reason: `Current UTC hour ${currentHour} is outside allowed window [${startHour}–${endHour}]`
147
+ };
148
+ }
149
+ return { passed: true };
150
+ }
151
+ case 'RateLimit': {
152
+ const rl = this._getOrInitRateLimit(walletKey);
153
+ if (rl.count >= rule.maxTransactionsPerMinute) {
154
+ return {
155
+ passed: false,
156
+ reason: `Rate limit exceeded: ${rl.count}/${rule.maxTransactionsPerMinute} transactions in the current minute window`
157
+ };
158
+ }
159
+ return { passed: true };
160
+ }
161
+ case 'RequireApproval':
162
+ if (intent.action.estimatedValue > rule.aboveSOLValue) {
163
+ // Check if a delegation from the approver is present
164
+ if (!intent.delegation || intent.delegation.grantor !== rule.approverAgentId) {
165
+ return {
166
+ passed: false,
167
+ reason: `Transaction value ${intent.action.estimatedValue} SOL exceeds ${rule.aboveSOLValue} SOL and requires approval from ${rule.approverAgentId}`
168
+ };
169
+ }
170
+ }
171
+ return { passed: true };
172
+ case 'DelegationScope':
173
+ if (rule.mustBeWithinDelegation && !intent.delegation) {
174
+ return {
175
+ passed: false,
176
+ reason: `This wallet requires a delegation token for all operations`
177
+ };
178
+ }
179
+ return { passed: true };
180
+ default:
181
+ return {
182
+ passed: false,
183
+ reason: `Unknown policy rule type: ${rule.type} — denied for safety`
184
+ };
185
+ }
186
+ }
187
+ /** Mutates in-memory spend/rate trackers after a successful transaction */
188
+ _recordSpend(walletKey, intent) {
189
+ const daily = this._getOrInitDailySpend(walletKey);
190
+ daily.totalSOL += intent.action.estimatedValue;
191
+ // Track per-mint spend for splTransfer and any token actions that carry a mint param
192
+ if (intent.action.params?.mint) {
193
+ const mintKey = intent.action.params.mint || 'unknown';
194
+ daily.perMint[mintKey] = (daily.perMint[mintKey] || 0) + Number(intent.action.params?.amount || 0);
195
+ }
196
+ const rl = this._getOrInitRateLimit(walletKey);
197
+ rl.count += 1;
198
+ }
199
+ async evaluate(intent, wallet, context) {
200
+ let policySet;
201
+ if (this._store) {
202
+ policySet = await this._store.get(`policy:${wallet.address.toBase58()}`);
203
+ }
204
+ else {
205
+ policySet = this._walletPolicies.get(wallet.address.toBase58());
206
+ }
207
+ // Fall back to the agent wallet's embedded setup
208
+ policySet = policySet || wallet.policySet;
209
+ if (!policySet || policySet.rules.length === 0) {
210
+ return {
211
+ allowed: policySet?.defaultAction === 'allow',
212
+ failedRules: [],
213
+ passedRules: [],
214
+ reason: policySet?.defaultAction === 'allow'
215
+ ? 'No rules defined — allowed by default policy'
216
+ : 'No rules defined — denied by default policy',
217
+ conditions: []
218
+ };
219
+ }
220
+ const failedRules = [];
221
+ const passedRules = [];
222
+ for (const rule of policySet.rules) {
223
+ const { passed, reason } = this._evaluateRule(rule, intent, wallet, context);
224
+ if (!passed) {
225
+ failedRules.push(rule);
226
+ return {
227
+ allowed: false,
228
+ failedRules,
229
+ passedRules,
230
+ reason: `Policy Denied: ${reason}`,
231
+ conditions: []
232
+ };
233
+ }
234
+ else {
235
+ passedRules.push(rule);
236
+ }
237
+ }
238
+ // All rules passed — record spend for rate-limit and daily trackers
239
+ this._recordSpend(wallet.address.toBase58(), intent);
240
+ return {
241
+ allowed: true,
242
+ failedRules,
243
+ passedRules,
244
+ reason: 'All policy rules passed.',
245
+ conditions: []
246
+ };
247
+ }
248
+ async evaluateDelegation(token, intent) {
249
+ const context = { network: 'unknown', currentBalance: 0 };
250
+ const passedRules = [];
251
+ const failedRules = [];
252
+ // Check expiry
253
+ if (Date.now() > token.validUntil) {
254
+ return {
255
+ allowed: false,
256
+ failedRules: [],
257
+ passedRules: [],
258
+ reason: `Delegation token has expired`,
259
+ conditions: []
260
+ };
261
+ }
262
+ // Check uses remaining
263
+ if (token.usesRemaining !== 'unlimited' && token.usesRemaining <= 0) {
264
+ return {
265
+ allowed: false,
266
+ failedRules: [],
267
+ passedRules: [],
268
+ reason: `Delegation token has no uses remaining`,
269
+ conditions: []
270
+ };
271
+ }
272
+ // Evaluate scope rules
273
+ const mockWallet = { address: token.walletAddress, policySet: token.scope };
274
+ for (const rule of token.scope.rules) {
275
+ const { passed, reason } = this._evaluateRule(rule, intent, mockWallet, context);
276
+ if (!passed) {
277
+ return {
278
+ allowed: false,
279
+ failedRules: [rule],
280
+ passedRules: [],
281
+ reason: `Delegation scope exceeded: ${reason}`,
282
+ conditions: []
283
+ };
284
+ }
285
+ passedRules.push(rule);
286
+ }
287
+ return {
288
+ allowed: true,
289
+ failedRules: [],
290
+ passedRules,
291
+ reason: 'Action is within authorized delegation scope',
292
+ conditions: []
293
+ };
294
+ }
295
+ async addRule(walletAddress, rule) {
296
+ const policy = await this._getOrCreatePolicySet(walletAddress);
297
+ // Auto-assign an ID if the rule doesn't have one
298
+ if (!rule.id) {
299
+ rule.id = (0, uuid_1.v4)();
300
+ }
301
+ policy.rules.push(rule);
302
+ policy.updatedAt = Date.now();
303
+ if (this._store) {
304
+ await this._store.set(`policy:${walletAddress.toBase58()}`, policy);
305
+ }
306
+ else {
307
+ this._walletPolicies.set(walletAddress.toBase58(), policy);
308
+ }
309
+ }
310
+ async removeRule(walletAddress, ruleId) {
311
+ const policy = await this._getOrCreatePolicySet(walletAddress);
312
+ const before = policy.rules.length;
313
+ policy.rules = policy.rules.filter(r => r.id !== ruleId);
314
+ if (policy.rules.length === before) {
315
+ console.warn(`[Policy] removeRule: No rule with id '${ruleId}' found for wallet ${walletAddress.toBase58()}`);
316
+ }
317
+ policy.updatedAt = Date.now();
318
+ if (this._store) {
319
+ await this._store.set(`policy:${walletAddress.toBase58()}`, policy);
320
+ }
321
+ else {
322
+ this._walletPolicies.set(walletAddress.toBase58(), policy);
323
+ }
324
+ }
325
+ async listRules(walletAddress) {
326
+ return await this._getOrCreatePolicySet(walletAddress);
327
+ }
328
+ async simulate(intent, wallet) {
329
+ // Simulate acts identically to evaluate but does NOT mutate spend trackers
330
+ const context = { network: 'simulate', currentBalance: 0 };
331
+ let policySet;
332
+ if (this._store) {
333
+ policySet = await this._store.get(`policy:${wallet.address.toBase58()}`);
334
+ }
335
+ else {
336
+ policySet = this._walletPolicies.get(wallet.address.toBase58());
337
+ }
338
+ policySet = policySet || wallet.policySet;
339
+ if (!policySet || policySet.rules.length === 0) {
340
+ return {
341
+ allowed: policySet?.defaultAction === 'allow',
342
+ failedRules: [],
343
+ passedRules: [],
344
+ reason: 'Simulated: no rules defined',
345
+ conditions: []
346
+ };
347
+ }
348
+ const failedRules = [];
349
+ const passedRules = [];
350
+ for (const rule of policySet.rules) {
351
+ const { passed, reason } = this._evaluateRule(rule, intent, wallet, context);
352
+ if (!passed) {
353
+ failedRules.push(rule);
354
+ return {
355
+ allowed: false,
356
+ failedRules,
357
+ passedRules,
358
+ reason: `Simulated — Policy Denied: ${reason}`,
359
+ conditions: []
360
+ };
361
+ }
362
+ passedRules.push(rule);
363
+ }
364
+ return {
365
+ allowed: true,
366
+ failedRules,
367
+ passedRules,
368
+ reason: 'Simulated — all rules would pass.',
369
+ conditions: []
370
+ };
371
+ }
372
+ }
373
+ exports.CorePolicyEngine = CorePolicyEngine;
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "@saw-protocol/policy",
3
+ "version": "0.1.0",
4
+ "description": "Default Policy Engine implementation for the SAW Protocol",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "scripts": {
8
+ "build": "tsc",
9
+ "clean": "rm -rf dist",
10
+ "test": "echo \"Error: no test specified\" && exit 1"
11
+ },
12
+ "dependencies": {
13
+ "@saw-protocol/core": "*",
14
+ "@solana/web3.js": "^1.91.1",
15
+ "uuid": "^9.0.1"
16
+ },
17
+ "devDependencies": {
18
+ "@types/uuid": "^9.0.8",
19
+ "typescript": "^5.4.3"
20
+ },
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/timileyindev/sawp.git"
24
+ },
25
+ "license": "ISC"
26
+ }
package/src/index.ts ADDED
@@ -0,0 +1,448 @@
1
+ import {
2
+ IPolicyEngine,
3
+ EvaluationContext,
4
+ EvaluationResult,
5
+ TransactionIntent,
6
+ AgentWallet,
7
+ DelegationToken,
8
+ PolicyRule,
9
+ PolicySet,
10
+ IStorageProvider
11
+ } from '@saw-protocol/core';
12
+ import { PublicKey } from '@solana/web3.js';
13
+ import { v4 as uuidv4 } from 'uuid';
14
+
15
+ // Per-wallet in-memory rate limit tracker (resets each minute window)
16
+ interface RateLimitState {
17
+ windowStart: number;
18
+ count: number;
19
+ }
20
+
21
+ // Per-wallet in-memory daily spend tracker
22
+ interface DailySpendState {
23
+ dayStart: number; // UTC midnight timestamp
24
+ totalSOL: number;
25
+ perMint: Record<string, number>;
26
+ }
27
+
28
+ /**
29
+ * The standard rule-based Policy Engine implementation for the SAWP Protocol.
30
+ * Evaluates intents against a dynamically managed set of strict `PolicySet` rules.
31
+ */
32
+ export class CorePolicyEngine implements IPolicyEngine {
33
+ private _store?: IStorageProvider;
34
+ private _walletPolicies = new Map<string, PolicySet>();
35
+ // In-memory rate-limit and daily-spend trackers (not persisted for performance)
36
+ private _rateLimitState = new Map<string, RateLimitState>();
37
+ private _dailySpendState = new Map<string, DailySpendState>();
38
+
39
+ constructor(store?: IStorageProvider) {
40
+ this._store = store;
41
+ }
42
+
43
+ /** Gets or initializes a policy set for a wallet */
44
+ private async _getOrCreatePolicySet(walletAddress: PublicKey): Promise<PolicySet> {
45
+ const key = `policy:${walletAddress.toBase58()}`;
46
+
47
+ // Check external store first
48
+ if (this._store) {
49
+ const stored = await this._store.get<PolicySet>(key);
50
+ if (stored) return stored;
51
+ }
52
+
53
+ // Check in-memory fallback
54
+ const memKey = walletAddress.toBase58();
55
+ if (this._walletPolicies.has(memKey)) {
56
+ return this._walletPolicies.get(memKey)!;
57
+ }
58
+
59
+ // Create default — defaultAction 'allow' so wallets work out of the box.
60
+ // Developers add rules to tighten as needed.
61
+ const newPolicy: PolicySet = {
62
+ id: uuidv4(),
63
+ rules: [],
64
+ defaultAction: 'allow',
65
+ createdAt: Date.now(),
66
+ updatedAt: Date.now()
67
+ };
68
+
69
+ if (this._store) {
70
+ await this._store.set(key, newPolicy);
71
+ } else {
72
+ this._walletPolicies.set(memKey, newPolicy);
73
+ }
74
+
75
+ return newPolicy;
76
+ }
77
+
78
+ private _getOrInitDailySpend(walletKey: string): DailySpendState {
79
+ const now = Date.now();
80
+ const startOfDay = new Date();
81
+ startOfDay.setUTCHours(0, 0, 0, 0);
82
+ const dayStart = startOfDay.getTime();
83
+
84
+ const existing = this._dailySpendState.get(walletKey);
85
+ if (existing && existing.dayStart === dayStart) return existing;
86
+
87
+ // New day — reset
88
+ const fresh: DailySpendState = { dayStart, totalSOL: 0, perMint: {} };
89
+ this._dailySpendState.set(walletKey, fresh);
90
+ return fresh;
91
+ }
92
+
93
+ private _getOrInitRateLimit(walletKey: string): RateLimitState {
94
+ const now = Date.now();
95
+ const existing = this._rateLimitState.get(walletKey);
96
+ if (existing && now - existing.windowStart < 60_000) return existing;
97
+
98
+ // New minute window
99
+ const fresh: RateLimitState = { windowStart: now, count: 0 };
100
+ this._rateLimitState.set(walletKey, fresh);
101
+ return fresh;
102
+ }
103
+
104
+ private _evaluateRule(
105
+ rule: PolicyRule,
106
+ intent: TransactionIntent,
107
+ wallet: AgentWallet,
108
+ context: EvaluationContext
109
+ ): { passed: boolean; reason?: string } {
110
+ const walletKey = wallet.address.toBase58();
111
+
112
+ switch (rule.type) {
113
+ case 'SpendLimit': {
114
+ if (intent.action.estimatedValue > rule.maxSOLPerTx) {
115
+ return {
116
+ passed: false,
117
+ reason: `Value ${intent.action.estimatedValue} SOL exceeds per-tx limit of ${rule.maxSOLPerTx} SOL`
118
+ };
119
+ }
120
+ // Enforce daily limit from in-memory tracker
121
+ const daily = this._getOrInitDailySpend(walletKey);
122
+ if (daily.totalSOL + intent.action.estimatedValue > rule.maxSOLPerDay) {
123
+ return {
124
+ passed: false,
125
+ reason: `Daily spend would reach ${(daily.totalSOL + intent.action.estimatedValue).toFixed(4)} SOL, limit is ${rule.maxSOLPerDay} SOL`
126
+ };
127
+ }
128
+ return { passed: true };
129
+ }
130
+
131
+ case 'TokenSpendLimit': {
132
+ const mintKey = rule.mint.toBase58();
133
+ const amount = Number(intent.action.params?.amount || 0);
134
+ if (amount > rule.maxPerTx) {
135
+ return {
136
+ passed: false,
137
+ reason: `Token amount ${amount} exceeds per-tx limit of ${rule.maxPerTx} for mint ${mintKey}`
138
+ };
139
+ }
140
+ const daily = this._getOrInitDailySpend(walletKey);
141
+ const mintSpent = daily.perMint[mintKey] || 0;
142
+ if (mintSpent + amount > rule.maxPerDay) {
143
+ return {
144
+ passed: false,
145
+ reason: `Daily token spend would reach ${mintSpent + amount}, limit is ${rule.maxPerDay} for mint ${mintKey}`
146
+ };
147
+ }
148
+ return { passed: true };
149
+ }
150
+
151
+ case 'ProgramAllowList':
152
+ if (
153
+ intent.action.programId &&
154
+ !rule.allowedProgramIds.some(p => p.toBase58() === intent.action.programId?.toBase58())
155
+ ) {
156
+ return {
157
+ passed: false,
158
+ reason: `Program ${intent.action.programId.toBase58()} is not in the ProgramAllowList`
159
+ };
160
+ }
161
+ return { passed: true };
162
+
163
+ case 'ProgramDenyList':
164
+ if (
165
+ intent.action.programId &&
166
+ rule.blockedProgramIds.some(p => p.toBase58() === intent.action.programId?.toBase58())
167
+ ) {
168
+ return {
169
+ passed: false,
170
+ reason: `Program ${intent.action.programId.toBase58()} is blocked by ProgramDenyList`
171
+ };
172
+ }
173
+ return { passed: true };
174
+
175
+ case 'Network':
176
+ if (!rule.allowedNetworks.includes(context.network as 'devnet' | 'mainnet')) {
177
+ return {
178
+ passed: false,
179
+ reason: `Network '${context.network}' is not in the allowed list [${rule.allowedNetworks.join(', ')}]`
180
+ };
181
+ }
182
+ return { passed: true };
183
+
184
+ case 'TimeWindow': {
185
+ const [startHour, endHour] = rule.allowedHoursUTC;
186
+ const currentHour = new Date().getUTCHours();
187
+ const inWindow =
188
+ startHour <= endHour
189
+ ? currentHour >= startHour && currentHour < endHour
190
+ : currentHour >= startHour || currentHour < endHour; // overnight window
191
+ if (!inWindow) {
192
+ return {
193
+ passed: false,
194
+ reason: `Current UTC hour ${currentHour} is outside allowed window [${startHour}–${endHour}]`
195
+ };
196
+ }
197
+ return { passed: true };
198
+ }
199
+
200
+ case 'RateLimit': {
201
+ const rl = this._getOrInitRateLimit(walletKey);
202
+ if (rl.count >= rule.maxTransactionsPerMinute) {
203
+ return {
204
+ passed: false,
205
+ reason: `Rate limit exceeded: ${rl.count}/${rule.maxTransactionsPerMinute} transactions in the current minute window`
206
+ };
207
+ }
208
+ return { passed: true };
209
+ }
210
+
211
+ case 'RequireApproval':
212
+ if (intent.action.estimatedValue > rule.aboveSOLValue) {
213
+ // Check if a delegation from the approver is present
214
+ if (!intent.delegation || intent.delegation.grantor !== rule.approverAgentId) {
215
+ return {
216
+ passed: false,
217
+ reason: `Transaction value ${intent.action.estimatedValue} SOL exceeds ${rule.aboveSOLValue} SOL and requires approval from ${rule.approverAgentId}`
218
+ };
219
+ }
220
+ }
221
+ return { passed: true };
222
+
223
+ case 'DelegationScope':
224
+ if (rule.mustBeWithinDelegation && !intent.delegation) {
225
+ return {
226
+ passed: false,
227
+ reason: `This wallet requires a delegation token for all operations`
228
+ };
229
+ }
230
+ return { passed: true };
231
+
232
+ default:
233
+ return {
234
+ passed: false,
235
+ reason: `Unknown policy rule type: ${(rule as any).type} — denied for safety`
236
+ };
237
+ }
238
+ }
239
+
240
+ /** Mutates in-memory spend/rate trackers after a successful transaction */
241
+ private _recordSpend(walletKey: string, intent: TransactionIntent): void {
242
+ const daily = this._getOrInitDailySpend(walletKey);
243
+ daily.totalSOL += intent.action.estimatedValue;
244
+
245
+ // Track per-mint spend for splTransfer and any token actions that carry a mint param
246
+ if (intent.action.params?.mint) {
247
+ const mintKey = (intent.action.params.mint as string) || 'unknown';
248
+ daily.perMint[mintKey] = (daily.perMint[mintKey] || 0) + Number(intent.action.params?.amount || 0);
249
+ }
250
+
251
+ const rl = this._getOrInitRateLimit(walletKey);
252
+ rl.count += 1;
253
+ }
254
+
255
+ async evaluate(intent: TransactionIntent, wallet: AgentWallet, context: EvaluationContext): Promise<EvaluationResult> {
256
+ let policySet: PolicySet | undefined | null;
257
+
258
+ if (this._store) {
259
+ policySet = await this._store.get<PolicySet>(`policy:${wallet.address.toBase58()}`);
260
+ } else {
261
+ policySet = this._walletPolicies.get(wallet.address.toBase58());
262
+ }
263
+
264
+ // Fall back to the agent wallet's embedded setup
265
+ policySet = policySet || wallet.policySet;
266
+
267
+ if (!policySet || policySet.rules.length === 0) {
268
+ return {
269
+ allowed: policySet?.defaultAction === 'allow',
270
+ failedRules: [],
271
+ passedRules: [],
272
+ reason: policySet?.defaultAction === 'allow'
273
+ ? 'No rules defined — allowed by default policy'
274
+ : 'No rules defined — denied by default policy',
275
+ conditions: []
276
+ };
277
+ }
278
+
279
+ const failedRules: PolicyRule[] = [];
280
+ const passedRules: PolicyRule[] = [];
281
+
282
+ for (const rule of policySet.rules) {
283
+ const { passed, reason } = this._evaluateRule(rule, intent, wallet, context);
284
+
285
+ if (!passed) {
286
+ failedRules.push(rule);
287
+ return {
288
+ allowed: false,
289
+ failedRules,
290
+ passedRules,
291
+ reason: `Policy Denied: ${reason}`,
292
+ conditions: []
293
+ };
294
+ } else {
295
+ passedRules.push(rule);
296
+ }
297
+ }
298
+
299
+ // All rules passed — record spend for rate-limit and daily trackers
300
+ this._recordSpend(wallet.address.toBase58(), intent);
301
+
302
+ return {
303
+ allowed: true,
304
+ failedRules,
305
+ passedRules,
306
+ reason: 'All policy rules passed.',
307
+ conditions: []
308
+ };
309
+ }
310
+
311
+ async evaluateDelegation(token: DelegationToken, intent: TransactionIntent): Promise<EvaluationResult> {
312
+ const context: EvaluationContext = { network: 'unknown', currentBalance: 0 };
313
+ const passedRules: PolicyRule[] = [];
314
+ const failedRules: PolicyRule[] = [];
315
+
316
+ // Check expiry
317
+ if (Date.now() > token.validUntil) {
318
+ return {
319
+ allowed: false,
320
+ failedRules: [],
321
+ passedRules: [],
322
+ reason: `Delegation token has expired`,
323
+ conditions: []
324
+ };
325
+ }
326
+
327
+ // Check uses remaining
328
+ if (token.usesRemaining !== 'unlimited' && token.usesRemaining <= 0) {
329
+ return {
330
+ allowed: false,
331
+ failedRules: [],
332
+ passedRules: [],
333
+ reason: `Delegation token has no uses remaining`,
334
+ conditions: []
335
+ };
336
+ }
337
+
338
+ // Evaluate scope rules
339
+ const mockWallet = { address: token.walletAddress, policySet: token.scope } as any;
340
+ for (const rule of token.scope.rules) {
341
+ const { passed, reason } = this._evaluateRule(rule, intent, mockWallet, context);
342
+ if (!passed) {
343
+ return {
344
+ allowed: false,
345
+ failedRules: [rule],
346
+ passedRules: [],
347
+ reason: `Delegation scope exceeded: ${reason}`,
348
+ conditions: []
349
+ };
350
+ }
351
+ passedRules.push(rule);
352
+ }
353
+
354
+ return {
355
+ allowed: true,
356
+ failedRules: [],
357
+ passedRules,
358
+ reason: 'Action is within authorized delegation scope',
359
+ conditions: []
360
+ };
361
+ }
362
+
363
+ async addRule(walletAddress: PublicKey, rule: PolicyRule): Promise<void> {
364
+ const policy = await this._getOrCreatePolicySet(walletAddress);
365
+ // Auto-assign an ID if the rule doesn't have one
366
+ if (!(rule as any).id) {
367
+ (rule as any).id = uuidv4();
368
+ }
369
+ policy.rules.push(rule);
370
+ policy.updatedAt = Date.now();
371
+
372
+ if (this._store) {
373
+ await this._store.set(`policy:${walletAddress.toBase58()}`, policy);
374
+ } else {
375
+ this._walletPolicies.set(walletAddress.toBase58(), policy);
376
+ }
377
+ }
378
+
379
+ async removeRule(walletAddress: PublicKey, ruleId: string): Promise<void> {
380
+ const policy = await this._getOrCreatePolicySet(walletAddress);
381
+ const before = policy.rules.length;
382
+ policy.rules = policy.rules.filter(r => (r as any).id !== ruleId);
383
+ if (policy.rules.length === before) {
384
+ console.warn(`[Policy] removeRule: No rule with id '${ruleId}' found for wallet ${walletAddress.toBase58()}`);
385
+ }
386
+ policy.updatedAt = Date.now();
387
+
388
+ if (this._store) {
389
+ await this._store.set(`policy:${walletAddress.toBase58()}`, policy);
390
+ } else {
391
+ this._walletPolicies.set(walletAddress.toBase58(), policy);
392
+ }
393
+ }
394
+
395
+ async listRules(walletAddress: PublicKey): Promise<PolicySet> {
396
+ return await this._getOrCreatePolicySet(walletAddress);
397
+ }
398
+
399
+ async simulate(intent: TransactionIntent, wallet: AgentWallet): Promise<EvaluationResult> {
400
+ // Simulate acts identically to evaluate but does NOT mutate spend trackers
401
+ const context: EvaluationContext = { network: 'simulate', currentBalance: 0 };
402
+ let policySet: PolicySet | undefined | null;
403
+
404
+ if (this._store) {
405
+ policySet = await this._store.get<PolicySet>(`policy:${wallet.address.toBase58()}`);
406
+ } else {
407
+ policySet = this._walletPolicies.get(wallet.address.toBase58());
408
+ }
409
+
410
+ policySet = policySet || wallet.policySet;
411
+
412
+ if (!policySet || policySet.rules.length === 0) {
413
+ return {
414
+ allowed: policySet?.defaultAction === 'allow',
415
+ failedRules: [],
416
+ passedRules: [],
417
+ reason: 'Simulated: no rules defined',
418
+ conditions: []
419
+ };
420
+ }
421
+
422
+ const failedRules: PolicyRule[] = [];
423
+ const passedRules: PolicyRule[] = [];
424
+
425
+ for (const rule of policySet.rules) {
426
+ const { passed, reason } = this._evaluateRule(rule, intent, wallet, context);
427
+ if (!passed) {
428
+ failedRules.push(rule);
429
+ return {
430
+ allowed: false,
431
+ failedRules,
432
+ passedRules,
433
+ reason: `Simulated — Policy Denied: ${reason}`,
434
+ conditions: []
435
+ };
436
+ }
437
+ passedRules.push(rule);
438
+ }
439
+
440
+ return {
441
+ allowed: true,
442
+ failedRules,
443
+ passedRules,
444
+ reason: 'Simulated — all rules would pass.',
445
+ conditions: []
446
+ };
447
+ }
448
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "CommonJS",
5
+ "moduleResolution": "node",
6
+ "declaration": true,
7
+ "outDir": "./dist",
8
+ "rootDir": "./src",
9
+ "strict": true,
10
+ "esModuleInterop": true,
11
+ "skipLibCheck": true,
12
+ "forceConsistentCasingInFileNames": true
13
+ },
14
+ "include": ["src/**/*"],
15
+ "exclude": ["node_modules", "dist"]
16
+ }