@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 +65 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.js +373 -0
- package/package.json +26 -0
- package/src/index.ts +448 -0
- package/tsconfig.json +16 -0
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
|
+
```
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|