@agirails/sdk 2.5.3 → 2.5.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ACTPClient.d.ts +18 -0
- package/dist/ACTPClient.d.ts.map +1 -1
- package/dist/ACTPClient.js +72 -23
- package/dist/ACTPClient.js.map +1 -1
- package/dist/adapters/BasicAdapter.d.ts +15 -0
- package/dist/adapters/BasicAdapter.d.ts.map +1 -1
- package/dist/adapters/BasicAdapter.js +33 -4
- package/dist/adapters/BasicAdapter.js.map +1 -1
- package/dist/adapters/StandardAdapter.d.ts +20 -3
- package/dist/adapters/StandardAdapter.d.ts.map +1 -1
- package/dist/adapters/StandardAdapter.js +90 -12
- package/dist/adapters/StandardAdapter.js.map +1 -1
- package/dist/cli/commands/publish.js +16 -4
- package/dist/cli/commands/publish.js.map +1 -1
- package/dist/cli/commands/register.js +16 -4
- package/dist/cli/commands/register.js.map +1 -1
- package/dist/cli/commands/tx.js +31 -3
- package/dist/cli/commands/tx.js.map +1 -1
- package/dist/config/networks.d.ts +10 -2
- package/dist/config/networks.d.ts.map +1 -1
- package/dist/config/networks.js +31 -22
- package/dist/config/networks.js.map +1 -1
- package/dist/level0/request.d.ts.map +1 -1
- package/dist/level0/request.js +2 -1
- package/dist/level0/request.js.map +1 -1
- package/dist/runtime/BlockchainRuntime.d.ts.map +1 -1
- package/dist/runtime/BlockchainRuntime.js +11 -5
- package/dist/runtime/BlockchainRuntime.js.map +1 -1
- package/dist/utils/IPFSClient.d.ts +3 -1
- package/dist/utils/IPFSClient.d.ts.map +1 -1
- package/dist/utils/IPFSClient.js +27 -7
- package/dist/utils/IPFSClient.js.map +1 -1
- package/dist/wallet/AutoWalletProvider.d.ts +11 -1
- package/dist/wallet/AutoWalletProvider.d.ts.map +1 -1
- package/dist/wallet/AutoWalletProvider.js +84 -19
- package/dist/wallet/AutoWalletProvider.js.map +1 -1
- package/dist/wallet/IWalletProvider.d.ts +34 -0
- package/dist/wallet/IWalletProvider.d.ts.map +1 -1
- package/dist/wallet/SmartWalletRouter.d.ts +128 -0
- package/dist/wallet/SmartWalletRouter.d.ts.map +1 -0
- package/dist/wallet/SmartWalletRouter.js +248 -0
- package/dist/wallet/SmartWalletRouter.js.map +1 -0
- package/dist/wallet/aa/DualNonceManager.d.ts +26 -1
- package/dist/wallet/aa/DualNonceManager.d.ts.map +1 -1
- package/dist/wallet/aa/DualNonceManager.js +140 -6
- package/dist/wallet/aa/DualNonceManager.js.map +1 -1
- package/package.json +3 -6
- package/src/ACTPClient.ts +0 -1579
- package/src/abi/ACTPKernel.json +0 -1356
- package/src/abi/AgentRegistry.json +0 -915
- package/src/abi/ERC20.json +0 -40
- package/src/abi/EscrowVault.json +0 -134
- package/src/abi/IdentityRegistry.json +0 -316
- package/src/adapters/AdapterRegistry.ts +0 -173
- package/src/adapters/AdapterRouter.ts +0 -416
- package/src/adapters/BaseAdapter.ts +0 -498
- package/src/adapters/BasicAdapter.ts +0 -514
- package/src/adapters/IAdapter.ts +0 -292
- package/src/adapters/StandardAdapter.ts +0 -555
- package/src/adapters/X402Adapter.ts +0 -731
- package/src/adapters/index.ts +0 -60
- package/src/builders/DeliveryProofBuilder.ts +0 -327
- package/src/builders/QuoteBuilder.ts +0 -483
- package/src/builders/index.ts +0 -17
- package/src/cli/commands/balance.ts +0 -110
- package/src/cli/commands/batch.ts +0 -487
- package/src/cli/commands/config.ts +0 -231
- package/src/cli/commands/deploy-check.ts +0 -364
- package/src/cli/commands/deploy-env.ts +0 -120
- package/src/cli/commands/diff.ts +0 -141
- package/src/cli/commands/init.ts +0 -469
- package/src/cli/commands/mint.ts +0 -116
- package/src/cli/commands/pay.ts +0 -113
- package/src/cli/commands/publish.ts +0 -475
- package/src/cli/commands/pull.ts +0 -124
- package/src/cli/commands/register.ts +0 -247
- package/src/cli/commands/simulate.ts +0 -345
- package/src/cli/commands/time.ts +0 -302
- package/src/cli/commands/tx.ts +0 -448
- package/src/cli/commands/watch.ts +0 -211
- package/src/cli/index.ts +0 -134
- package/src/cli/utils/client.ts +0 -252
- package/src/cli/utils/config.ts +0 -389
- package/src/cli/utils/output.ts +0 -465
- package/src/cli/utils/wallet.ts +0 -109
- package/src/config/agirailsmd.ts +0 -262
- package/src/config/networks.ts +0 -275
- package/src/config/pendingPublish.ts +0 -237
- package/src/config/publishPipeline.ts +0 -359
- package/src/config/syncOperations.ts +0 -279
- package/src/erc8004/ERC8004Bridge.ts +0 -462
- package/src/erc8004/ReputationReporter.ts +0 -468
- package/src/erc8004/index.ts +0 -61
- package/src/errors/index.ts +0 -427
- package/src/index.ts +0 -364
- package/src/level0/Provider.ts +0 -117
- package/src/level0/ServiceDirectory.ts +0 -131
- package/src/level0/index.ts +0 -10
- package/src/level0/provide.ts +0 -132
- package/src/level0/request.ts +0 -432
- package/src/level1/Agent.ts +0 -1426
- package/src/level1/index.ts +0 -10
- package/src/level1/pricing/PriceCalculator.ts +0 -255
- package/src/level1/pricing/PricingStrategy.ts +0 -198
- package/src/level1/types/Job.ts +0 -179
- package/src/level1/types/Options.ts +0 -291
- package/src/level1/types/index.ts +0 -8
- package/src/protocol/ACTPKernel.ts +0 -808
- package/src/protocol/AgentRegistry.ts +0 -559
- package/src/protocol/DIDManager.ts +0 -629
- package/src/protocol/DIDResolver.ts +0 -554
- package/src/protocol/EASHelper.ts +0 -378
- package/src/protocol/EscrowVault.ts +0 -255
- package/src/protocol/EventMonitor.ts +0 -204
- package/src/protocol/MessageSigner.ts +0 -510
- package/src/protocol/ProofGenerator.ts +0 -339
- package/src/protocol/QuoteBuilder.ts +0 -15
- package/src/registry/AgentRegistryClient.ts +0 -202
- package/src/runtime/BlockchainRuntime.ts +0 -1015
- package/src/runtime/IACTPRuntime.ts +0 -306
- package/src/runtime/MockRuntime.ts +0 -1298
- package/src/runtime/MockStateManager.ts +0 -577
- package/src/runtime/index.ts +0 -25
- package/src/runtime/types/MockState.ts +0 -237
- package/src/storage/ArchiveBundleBuilder.ts +0 -561
- package/src/storage/ArweaveClient.ts +0 -946
- package/src/storage/FilebaseClient.ts +0 -790
- package/src/storage/index.ts +0 -96
- package/src/storage/types.ts +0 -348
- package/src/types/adapter.ts +0 -310
- package/src/types/agent.ts +0 -79
- package/src/types/did.ts +0 -223
- package/src/types/eip712.ts +0 -175
- package/src/types/erc8004.ts +0 -293
- package/src/types/escrow.ts +0 -27
- package/src/types/index.ts +0 -17
- package/src/types/message.ts +0 -145
- package/src/types/state.ts +0 -87
- package/src/types/transaction.ts +0 -69
- package/src/types/x402.ts +0 -251
- package/src/utils/ErrorRecoveryGuide.ts +0 -676
- package/src/utils/Helpers.ts +0 -688
- package/src/utils/IPFSClient.ts +0 -368
- package/src/utils/Logger.ts +0 -484
- package/src/utils/NonceManager.ts +0 -591
- package/src/utils/RateLimiter.ts +0 -534
- package/src/utils/ReceivedNonceTracker.ts +0 -567
- package/src/utils/SDKLifecycle.ts +0 -416
- package/src/utils/SecureNonce.ts +0 -78
- package/src/utils/Semaphore.ts +0 -276
- package/src/utils/UsedAttestationTracker.ts +0 -385
- package/src/utils/canonicalJson.ts +0 -38
- package/src/utils/circuitBreaker.ts +0 -324
- package/src/utils/computeTypeHash.ts +0 -48
- package/src/utils/fsSafe.ts +0 -80
- package/src/utils/index.ts +0 -80
- package/src/utils/retry.ts +0 -364
- package/src/utils/security.ts +0 -418
- package/src/utils/validation.ts +0 -540
- package/src/wallet/AutoWalletProvider.ts +0 -299
- package/src/wallet/EOAWalletProvider.ts +0 -69
- package/src/wallet/IWalletProvider.ts +0 -135
- package/src/wallet/aa/BundlerClient.ts +0 -274
- package/src/wallet/aa/DualNonceManager.ts +0 -173
- package/src/wallet/aa/PaymasterClient.ts +0 -174
- package/src/wallet/aa/TransactionBatcher.ts +0 -353
- package/src/wallet/aa/UserOpBuilder.ts +0 -246
- package/src/wallet/aa/constants.ts +0 -60
- package/src/wallet/keystore.ts +0 -240
package/src/level1/Agent.ts
DELETED
|
@@ -1,1426 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Agent - Standard API for AI agents
|
|
3
|
-
*
|
|
4
|
-
* Provides agent-level abstractions: lifecycle, service provision,
|
|
5
|
-
* job handling, events, and statistics.
|
|
6
|
-
*
|
|
7
|
-
* @packageDocumentation
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { EventEmitter } from 'events';
|
|
11
|
-
import * as path from 'path';
|
|
12
|
-
import * as os from 'os';
|
|
13
|
-
import * as fs from 'fs';
|
|
14
|
-
import { ethers } from 'ethers';
|
|
15
|
-
import { ACTPClient } from '../ACTPClient';
|
|
16
|
-
import { resolvePrivateKey } from '../wallet/keystore';
|
|
17
|
-
import { Job, JobHandler, JobContext } from './types/Job';
|
|
18
|
-
import { RequestOptions, RequestResult, NetworkOption } from './types/Options';
|
|
19
|
-
import { PricingStrategy } from './pricing/PricingStrategy';
|
|
20
|
-
import { AgentLifecycleError, ServiceConfigError, ValidationError } from '../errors';
|
|
21
|
-
import { validateServiceName, validatePath, LRUCache } from '../utils/security';
|
|
22
|
-
import { Logger, sdkLogger } from '../utils/Logger';
|
|
23
|
-
import { ServiceHash } from '../utils/Helpers';
|
|
24
|
-
import { Semaphore } from '../utils/Semaphore';
|
|
25
|
-
import { ProofGenerator } from '../protocol/ProofGenerator';
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Agent lifecycle states
|
|
29
|
-
*/
|
|
30
|
-
export type AgentStatus = 'idle' | 'starting' | 'running' | 'paused' | 'stopping' | 'stopped';
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Service filter configuration
|
|
34
|
-
*/
|
|
35
|
-
export interface ServiceFilter {
|
|
36
|
-
/**
|
|
37
|
-
* Minimum budget in USDC (e.g., 5.00 for $5)
|
|
38
|
-
*/
|
|
39
|
-
minBudget?: number;
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Maximum budget in USDC (e.g., 100.00 for $100)
|
|
43
|
-
*/
|
|
44
|
-
maxBudget?: number;
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Custom filter function
|
|
48
|
-
*/
|
|
49
|
-
custom?: (job: Job) => boolean;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Service configuration
|
|
54
|
-
*/
|
|
55
|
-
export interface ServiceConfig {
|
|
56
|
-
/**
|
|
57
|
-
* Service name (e.g., 'translation', 'echo')
|
|
58
|
-
*/
|
|
59
|
-
name: string;
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Human-readable description
|
|
63
|
-
*/
|
|
64
|
-
description?: string;
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Pricing strategy (cost + margin model)
|
|
68
|
-
*/
|
|
69
|
-
pricing?: PricingStrategy;
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Service capabilities/tags
|
|
73
|
-
*/
|
|
74
|
-
capabilities?: string[];
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Job filter (function or filter config)
|
|
78
|
-
*/
|
|
79
|
-
filter?: ServiceFilter | ((job: Job) => boolean);
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Timeout per job (milliseconds)
|
|
83
|
-
*/
|
|
84
|
-
timeout?: number;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Agent configuration
|
|
89
|
-
*/
|
|
90
|
-
export interface AgentConfig {
|
|
91
|
-
/**
|
|
92
|
-
* Agent name
|
|
93
|
-
*/
|
|
94
|
-
name: string;
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Agent description
|
|
98
|
-
*/
|
|
99
|
-
description?: string;
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Wallet configuration
|
|
103
|
-
*/
|
|
104
|
-
wallet?: 'auto' | 'connect' | string | { privateKey: string };
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Network
|
|
108
|
-
*/
|
|
109
|
-
network?: NetworkOption;
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* RPC URL for blockchain connection (required for testnet/mainnet)
|
|
113
|
-
*
|
|
114
|
-
* If not provided, defaults to public RPC from network config:
|
|
115
|
-
* - testnet: https://sepolia.base.org
|
|
116
|
-
* - mainnet: https://mainnet.base.org
|
|
117
|
-
*
|
|
118
|
-
* For production, consider using a dedicated RPC provider (Alchemy, Infura, etc.)
|
|
119
|
-
* for better reliability and rate limits.
|
|
120
|
-
*
|
|
121
|
-
* @example
|
|
122
|
-
* rpcUrl: 'https://base-sepolia.g.alchemy.com/v2/YOUR_API_KEY'
|
|
123
|
-
*/
|
|
124
|
-
rpcUrl?: string;
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* State directory (mock mode only)
|
|
128
|
-
*/
|
|
129
|
-
stateDirectory?: string;
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* Behavior configuration
|
|
133
|
-
*/
|
|
134
|
-
behavior?: {
|
|
135
|
-
/**
|
|
136
|
-
* Auto-accept jobs
|
|
137
|
-
*/
|
|
138
|
-
autoAccept?: boolean | ((job: Job) => boolean | Promise<boolean>);
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* Max concurrent jobs
|
|
142
|
-
*/
|
|
143
|
-
concurrency?: number;
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* Retry configuration
|
|
147
|
-
*/
|
|
148
|
-
retry?: {
|
|
149
|
-
attempts?: number;
|
|
150
|
-
delay?: number;
|
|
151
|
-
backoff?: 'linear' | 'exponential';
|
|
152
|
-
};
|
|
153
|
-
};
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* Persistence configuration
|
|
157
|
-
*/
|
|
158
|
-
persistence?: {
|
|
159
|
-
enabled?: boolean;
|
|
160
|
-
path?: string;
|
|
161
|
-
};
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* Logging configuration
|
|
165
|
-
*/
|
|
166
|
-
logging?: {
|
|
167
|
-
level?: 'debug' | 'info' | 'warn' | 'error';
|
|
168
|
-
};
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/**
|
|
172
|
-
* Agent statistics
|
|
173
|
-
*/
|
|
174
|
-
export interface AgentStats {
|
|
175
|
-
jobsReceived: number;
|
|
176
|
-
jobsCompleted: number;
|
|
177
|
-
jobsFailed: number;
|
|
178
|
-
totalEarned: number;
|
|
179
|
-
totalSpent: number;
|
|
180
|
-
averageJobTime: number;
|
|
181
|
-
successRate: number;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
/**
|
|
185
|
-
* Agent balance information
|
|
186
|
-
*/
|
|
187
|
-
export interface AgentBalance {
|
|
188
|
-
eth: string;
|
|
189
|
-
usdc: string;
|
|
190
|
-
locked: string;
|
|
191
|
-
pending: string;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
/**
|
|
195
|
-
* Agent class - Standard API
|
|
196
|
-
*
|
|
197
|
-
* Represents an autonomous AI agent that can provide services,
|
|
198
|
-
* request services from other agents, and manage its lifecycle.
|
|
199
|
-
*
|
|
200
|
-
* @example
|
|
201
|
-
* ```typescript
|
|
202
|
-
* const agent = new Agent({ name: 'Translator', network: 'mock' });
|
|
203
|
-
*
|
|
204
|
-
* agent.provide('translation', async (job, ctx) => {
|
|
205
|
-
* ctx.progress(50, 'Translating...');
|
|
206
|
-
* return { translated: translate(job.input.text) };
|
|
207
|
-
* });
|
|
208
|
-
*
|
|
209
|
-
* agent.on('payment:received', (amount) => {
|
|
210
|
-
* console.log(`Earned ${amount} USDC!`);
|
|
211
|
-
* });
|
|
212
|
-
*
|
|
213
|
-
* await agent.start();
|
|
214
|
-
* ```
|
|
215
|
-
*/
|
|
216
|
-
export class Agent extends EventEmitter {
|
|
217
|
-
/**
|
|
218
|
-
* Agent name
|
|
219
|
-
*/
|
|
220
|
-
public readonly name: string;
|
|
221
|
-
|
|
222
|
-
/**
|
|
223
|
-
* Agent description
|
|
224
|
-
*/
|
|
225
|
-
public readonly description?: string;
|
|
226
|
-
|
|
227
|
-
/**
|
|
228
|
-
* Network the agent operates on
|
|
229
|
-
*/
|
|
230
|
-
public readonly network: NetworkOption;
|
|
231
|
-
|
|
232
|
-
/**
|
|
233
|
-
* Current agent status
|
|
234
|
-
*/
|
|
235
|
-
private _status: AgentStatus = 'idle';
|
|
236
|
-
|
|
237
|
-
/**
|
|
238
|
-
* ACTP Client instance
|
|
239
|
-
*/
|
|
240
|
-
private _client?: ACTPClient;
|
|
241
|
-
|
|
242
|
-
/**
|
|
243
|
-
* Registered services
|
|
244
|
-
*/
|
|
245
|
-
private services = new Map<string, { config: ServiceConfig; handler: JobHandler }>();
|
|
246
|
-
|
|
247
|
-
/**
|
|
248
|
-
* Active jobs
|
|
249
|
-
*
|
|
250
|
-
* SECURITY FIX (C-2): Changed from Map to LRUCache to prevent unbounded growth
|
|
251
|
-
* Maximum 1000 active jobs with LRU eviction
|
|
252
|
-
*/
|
|
253
|
-
private activeJobs = new LRUCache<string, Job>(1000);
|
|
254
|
-
|
|
255
|
-
/**
|
|
256
|
-
* Processed job IDs (for deduplication)
|
|
257
|
-
*
|
|
258
|
-
* SECURITY FIX (C-1): Track jobs we've attempted to process
|
|
259
|
-
* This prevents race conditions where the same job is processed multiple times
|
|
260
|
-
* before the state transition completes
|
|
261
|
-
*/
|
|
262
|
-
private processedJobs = new LRUCache<string, boolean>(10000);
|
|
263
|
-
|
|
264
|
-
/**
|
|
265
|
-
* Processing locks (for atomic locking)
|
|
266
|
-
*
|
|
267
|
-
* SECURITY FIX (C-1): Mutex for job processing.
|
|
268
|
-
* When we see a job, we IMMEDIATELY add to this set (atomic in single-threaded JS).
|
|
269
|
-
* This prevents race conditions where two poll cycles both pass the processedJobs.has()
|
|
270
|
-
* check before either calls processedJobs.set().
|
|
271
|
-
*/
|
|
272
|
-
private processingLocks = new Set<string>();
|
|
273
|
-
|
|
274
|
-
/**
|
|
275
|
-
* Concurrency semaphore
|
|
276
|
-
*
|
|
277
|
-
* SECURITY FIX (MEDIUM-4): Limits concurrent job execution to prevent
|
|
278
|
-
* resource exhaustion (memory/CPU DoS). Uses behavior.concurrency setting.
|
|
279
|
-
*/
|
|
280
|
-
private concurrencySemaphore!: Semaphore;
|
|
281
|
-
|
|
282
|
-
/**
|
|
283
|
-
* Statistics
|
|
284
|
-
*/
|
|
285
|
-
private _stats: AgentStats = {
|
|
286
|
-
jobsReceived: 0,
|
|
287
|
-
jobsCompleted: 0,
|
|
288
|
-
jobsFailed: 0,
|
|
289
|
-
totalEarned: 0,
|
|
290
|
-
totalSpent: 0,
|
|
291
|
-
averageJobTime: 0,
|
|
292
|
-
successRate: 0,
|
|
293
|
-
};
|
|
294
|
-
|
|
295
|
-
/**
|
|
296
|
-
* Cached balance (updated periodically during polling)
|
|
297
|
-
*/
|
|
298
|
-
private _balance: AgentBalance = {
|
|
299
|
-
eth: '0',
|
|
300
|
-
usdc: '0',
|
|
301
|
-
locked: '0',
|
|
302
|
-
pending: '0',
|
|
303
|
-
};
|
|
304
|
-
|
|
305
|
-
/**
|
|
306
|
-
* Configuration
|
|
307
|
-
*/
|
|
308
|
-
private config: AgentConfig;
|
|
309
|
-
|
|
310
|
-
/**
|
|
311
|
-
* Polling interval ID (for job polling)
|
|
312
|
-
*/
|
|
313
|
-
private pollingIntervalId?: NodeJS.Timeout;
|
|
314
|
-
|
|
315
|
-
/**
|
|
316
|
-
* Logger instance
|
|
317
|
-
*/
|
|
318
|
-
private logger: Logger;
|
|
319
|
-
|
|
320
|
-
/**
|
|
321
|
-
* Creates a new Agent instance
|
|
322
|
-
*
|
|
323
|
-
* @param config - Agent configuration
|
|
324
|
-
*/
|
|
325
|
-
constructor(config: AgentConfig) {
|
|
326
|
-
super();
|
|
327
|
-
|
|
328
|
-
if (!config.name) {
|
|
329
|
-
throw new ServiceConfigError('name', 'Agent name is required');
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
// SECURITY FIX (H-6): Use dedicated AGIRAILS directory as base
|
|
333
|
-
// This prevents writes anywhere in the project directory
|
|
334
|
-
const AGIRAILS_BASE = path.join(os.homedir(), '.agirails');
|
|
335
|
-
|
|
336
|
-
// Ensure base directory exists
|
|
337
|
-
if (!fs.existsSync(AGIRAILS_BASE)) {
|
|
338
|
-
fs.mkdirSync(AGIRAILS_BASE, { recursive: true });
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
// Validate state directory path if provided
|
|
342
|
-
if (config.stateDirectory) {
|
|
343
|
-
try {
|
|
344
|
-
// Validate the path is safe (no traversal, within AGIRAILS_BASE)
|
|
345
|
-
config.stateDirectory = validatePath(config.stateDirectory, AGIRAILS_BASE);
|
|
346
|
-
} catch (error) {
|
|
347
|
-
throw new ServiceConfigError(
|
|
348
|
-
'stateDirectory',
|
|
349
|
-
`Invalid state directory: ${error instanceof Error ? error.message : String(error)}`
|
|
350
|
-
);
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
this.name = config.name;
|
|
355
|
-
this.description = config.description;
|
|
356
|
-
this.network = config.network || 'mock';
|
|
357
|
-
this.config = config;
|
|
358
|
-
this.logger = new Logger({
|
|
359
|
-
source: `Agent:${config.name}`,
|
|
360
|
-
minLevel: config.logging?.level || 'info',
|
|
361
|
-
});
|
|
362
|
-
|
|
363
|
-
// SECURITY FIX (MEDIUM-4): Initialize concurrency semaphore
|
|
364
|
-
// Default to 10 concurrent jobs if not specified
|
|
365
|
-
const maxConcurrency = config.behavior?.concurrency || 10;
|
|
366
|
-
this.concurrencySemaphore = new Semaphore(maxConcurrency);
|
|
367
|
-
this.logger.debug('Initialized concurrency semaphore', { maxConcurrency });
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
// =========================================================================
|
|
371
|
-
// Lifecycle Methods
|
|
372
|
-
// =========================================================================
|
|
373
|
-
|
|
374
|
-
/**
|
|
375
|
-
* Start the agent
|
|
376
|
-
*
|
|
377
|
-
* Initializes ACTP client and begins polling for jobs.
|
|
378
|
-
*
|
|
379
|
-
* @throws {AgentLifecycleError} If agent is not in idle or stopped state
|
|
380
|
-
*/
|
|
381
|
-
async start(): Promise<void> {
|
|
382
|
-
if (this._status !== 'idle' && this._status !== 'stopped') {
|
|
383
|
-
throw new AgentLifecycleError(this._status, 'start');
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
this._status = 'starting';
|
|
387
|
-
this.emit('starting');
|
|
388
|
-
|
|
389
|
-
try {
|
|
390
|
-
let rpcUrl = this.config.rpcUrl;
|
|
391
|
-
if (!rpcUrl && (this.network === 'testnet' || this.network === 'mainnet')) {
|
|
392
|
-
const { getNetwork } = await import('../config/networks');
|
|
393
|
-
const networkName = this.network === 'testnet' ? 'base-sepolia' : 'base-mainnet';
|
|
394
|
-
const networkConfig = getNetwork(networkName);
|
|
395
|
-
rpcUrl = networkConfig.rpcUrl;
|
|
396
|
-
this.logger.info(`Using default RPC URL for ${networkName}: ${rpcUrl}`);
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
this._client = await ACTPClient.create({
|
|
400
|
-
mode: this.network === 'testnet' ? 'testnet' : this.network === 'mainnet' ? 'mainnet' : 'mock',
|
|
401
|
-
requesterAddress: this.address || await this.generateAddress(),
|
|
402
|
-
stateDirectory: this.config.stateDirectory,
|
|
403
|
-
privateKey: await this.getPrivateKey(),
|
|
404
|
-
rpcUrl,
|
|
405
|
-
});
|
|
406
|
-
|
|
407
|
-
this.startPolling();
|
|
408
|
-
|
|
409
|
-
this._status = 'running';
|
|
410
|
-
this.emit('started');
|
|
411
|
-
} catch (error) {
|
|
412
|
-
this._status = 'stopped';
|
|
413
|
-
this.emit('error', error);
|
|
414
|
-
throw error;
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
/**
|
|
419
|
-
* Stop the agent
|
|
420
|
-
*
|
|
421
|
-
* Stops polling and waits for active jobs to complete.
|
|
422
|
-
*/
|
|
423
|
-
async stop(): Promise<void> {
|
|
424
|
-
if (this._status === 'stopped' || this._status === 'stopping') {
|
|
425
|
-
return;
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
this._status = 'stopping';
|
|
429
|
-
this.emit('stopping');
|
|
430
|
-
|
|
431
|
-
// Stop polling
|
|
432
|
-
this.stopPolling();
|
|
433
|
-
|
|
434
|
-
// Wait for active jobs to complete (with timeout)
|
|
435
|
-
await this.waitForActiveJobs(30000); // 30s timeout
|
|
436
|
-
|
|
437
|
-
this._status = 'stopped';
|
|
438
|
-
this.emit('stopped');
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
/**
|
|
442
|
-
* Pause the agent
|
|
443
|
-
*
|
|
444
|
-
* Stops accepting new jobs but keeps active jobs running.
|
|
445
|
-
*/
|
|
446
|
-
pause(): void {
|
|
447
|
-
if (this._status !== 'running') {
|
|
448
|
-
throw new AgentLifecycleError(this._status, 'pause');
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
this.stopPolling();
|
|
452
|
-
this._status = 'paused';
|
|
453
|
-
this.emit('paused');
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
/**
|
|
457
|
-
* Resume the agent
|
|
458
|
-
*
|
|
459
|
-
* Resumes accepting new jobs after being paused.
|
|
460
|
-
*/
|
|
461
|
-
resume(): void {
|
|
462
|
-
if (this._status !== 'paused') {
|
|
463
|
-
throw new AgentLifecycleError(this._status, 'resume');
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
this.startPolling();
|
|
467
|
-
this._status = 'running';
|
|
468
|
-
this.emit('resumed');
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
/**
|
|
472
|
-
* Restart the agent
|
|
473
|
-
*/
|
|
474
|
-
async restart(): Promise<void> {
|
|
475
|
-
await this.stop();
|
|
476
|
-
await this.start();
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
// =========================================================================
|
|
480
|
-
// Service Registration
|
|
481
|
-
// =========================================================================
|
|
482
|
-
|
|
483
|
-
/**
|
|
484
|
-
* Register a service handler
|
|
485
|
-
*
|
|
486
|
-
* @param serviceOrConfig - Service name or full configuration
|
|
487
|
-
* @param handler - Job handler function
|
|
488
|
-
* @param options - Optional pricing/filter configuration
|
|
489
|
-
*
|
|
490
|
-
* @example
|
|
491
|
-
* ```typescript
|
|
492
|
-
* // Simple
|
|
493
|
-
* agent.provide('echo', async (job) => job.input);
|
|
494
|
-
*
|
|
495
|
-
* // With pricing
|
|
496
|
-
* agent.provide({
|
|
497
|
-
* name: 'translation',
|
|
498
|
-
* pricing: {
|
|
499
|
-
* cost: { base: 0.5, perUnit: { unit: 'word', rate: 0.005 } },
|
|
500
|
-
* margin: 0.40
|
|
501
|
-
* }
|
|
502
|
-
* }, async (job, ctx) => {
|
|
503
|
-
* // ... translation logic
|
|
504
|
-
* });
|
|
505
|
-
* ```
|
|
506
|
-
*/
|
|
507
|
-
provide(
|
|
508
|
-
serviceOrConfig: string | ServiceConfig,
|
|
509
|
-
handler: JobHandler,
|
|
510
|
-
options?: Partial<ServiceConfig>
|
|
511
|
-
): this {
|
|
512
|
-
const config: ServiceConfig =
|
|
513
|
-
typeof serviceOrConfig === 'string'
|
|
514
|
-
? { name: serviceOrConfig, ...options }
|
|
515
|
-
: serviceOrConfig;
|
|
516
|
-
|
|
517
|
-
if (!config.name) {
|
|
518
|
-
throw new ServiceConfigError('name', 'Service name is required');
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
// SECURITY FIX (H-2): Validate service name to prevent injection
|
|
522
|
-
try {
|
|
523
|
-
config.name = validateServiceName(config.name);
|
|
524
|
-
} catch (error) {
|
|
525
|
-
throw new ServiceConfigError(
|
|
526
|
-
'name',
|
|
527
|
-
`Invalid service name: ${error instanceof Error ? error.message : String(error)}`
|
|
528
|
-
);
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
if (this.services.has(config.name)) {
|
|
532
|
-
throw new ServiceConfigError('name', `Service "${config.name}" already registered`);
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
this.services.set(config.name, { config, handler });
|
|
536
|
-
this.emit('service:registered', config.name);
|
|
537
|
-
this.logger.info('Service registered', { service: config.name });
|
|
538
|
-
|
|
539
|
-
return this;
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
/**
|
|
543
|
-
* Request a service from another agent
|
|
544
|
-
*
|
|
545
|
-
* @param service - Service name
|
|
546
|
-
* @param options - Request options
|
|
547
|
-
* @returns Promise resolving to result
|
|
548
|
-
*/
|
|
549
|
-
async request(service: string, options: Omit<RequestOptions, 'network'>): Promise<RequestResult> {
|
|
550
|
-
if (!this._client) {
|
|
551
|
-
throw new AgentLifecycleError(this._status, 'request (agent not started)');
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
// Import Basic API request function
|
|
555
|
-
const { request: basicRequest } = await import('../level0/request');
|
|
556
|
-
|
|
557
|
-
// Call Basic API request with agent's network
|
|
558
|
-
const result = await basicRequest(service, {
|
|
559
|
-
...options,
|
|
560
|
-
network: this.network,
|
|
561
|
-
});
|
|
562
|
-
|
|
563
|
-
// Update stats
|
|
564
|
-
this._stats.totalSpent += options.budget;
|
|
565
|
-
|
|
566
|
-
return result;
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
// =========================================================================
|
|
570
|
-
// Properties
|
|
571
|
-
// =========================================================================
|
|
572
|
-
|
|
573
|
-
/**
|
|
574
|
-
* Current agent status
|
|
575
|
-
*/
|
|
576
|
-
get status(): AgentStatus {
|
|
577
|
-
return this._status;
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
/**
|
|
581
|
-
* Agent's Ethereum address
|
|
582
|
-
*/
|
|
583
|
-
get address(): string {
|
|
584
|
-
return this._client?.getAddress() || '';
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
/**
|
|
588
|
-
* Registered service names
|
|
589
|
-
*/
|
|
590
|
-
get serviceNames(): string[] {
|
|
591
|
-
return Array.from(this.services.keys());
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
/**
|
|
595
|
-
* Active jobs
|
|
596
|
-
*
|
|
597
|
-
* SECURITY FIX (N-2): Now uses LRUCache.values() iterator.
|
|
598
|
-
* Returns a snapshot of currently active jobs.
|
|
599
|
-
*/
|
|
600
|
-
get jobs(): Job[] {
|
|
601
|
-
return this.activeJobs.values();
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
/**
|
|
605
|
-
* Statistics
|
|
606
|
-
*/
|
|
607
|
-
get stats(): AgentStats {
|
|
608
|
-
return { ...this._stats };
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
/**
|
|
612
|
-
* Get agent balance
|
|
613
|
-
*
|
|
614
|
-
* Returns current USDC balance plus locked/pending amounts from active transactions.
|
|
615
|
-
* Note: This is an async operation wrapped in a sync getter for convenience.
|
|
616
|
-
* For real-time balance, use getBalanceAsync() instead.
|
|
617
|
-
*/
|
|
618
|
-
get balance(): AgentBalance {
|
|
619
|
-
// Return cached balance (updated periodically during polling)
|
|
620
|
-
return { ...this._balance };
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
/**
|
|
624
|
-
* Get agent balance asynchronously (real-time)
|
|
625
|
-
*
|
|
626
|
-
* @returns Promise resolving to current balance
|
|
627
|
-
*/
|
|
628
|
-
async getBalanceAsync(): Promise<AgentBalance> {
|
|
629
|
-
if (!this._client?.runtime) {
|
|
630
|
-
return {
|
|
631
|
-
eth: '0',
|
|
632
|
-
usdc: '0',
|
|
633
|
-
locked: '0',
|
|
634
|
-
pending: '0',
|
|
635
|
-
};
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
try {
|
|
639
|
-
// Get USDC balance (if runtime supports it)
|
|
640
|
-
let usdc = '0';
|
|
641
|
-
if ('getBalance' in this._client.runtime) {
|
|
642
|
-
usdc = await (this._client.runtime as any).getBalance(this.address);
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
// Get transactions where this agent is provider
|
|
646
|
-
let locked = BigInt(0);
|
|
647
|
-
let pending = BigInt(0);
|
|
648
|
-
|
|
649
|
-
if ('getTransactionsByProvider' in this._client.runtime) {
|
|
650
|
-
// Get all active transactions for this provider
|
|
651
|
-
const allTx = await (this._client.runtime as any).getTransactionsByProvider(
|
|
652
|
-
this.address,
|
|
653
|
-
undefined, // all states
|
|
654
|
-
1000
|
|
655
|
-
);
|
|
656
|
-
|
|
657
|
-
for (const tx of allTx) {
|
|
658
|
-
const amount = BigInt(tx.amount || '0');
|
|
659
|
-
|
|
660
|
-
// Locked: funds in escrow for active work (COMMITTED, IN_PROGRESS, DELIVERED)
|
|
661
|
-
if (tx.state === 'COMMITTED' || tx.state === 'IN_PROGRESS' || tx.state === 'DELIVERED') {
|
|
662
|
-
locked += amount;
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
// Pending: potential earnings waiting to be accepted (INITIATED, QUOTED)
|
|
666
|
-
if (tx.state === 'INITIATED' || tx.state === 'QUOTED') {
|
|
667
|
-
pending += amount;
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
this._balance = {
|
|
673
|
-
eth: '0', // ETH balance not tracked in USDC-only system
|
|
674
|
-
usdc: usdc,
|
|
675
|
-
locked: locked.toString(),
|
|
676
|
-
pending: pending.toString(),
|
|
677
|
-
};
|
|
678
|
-
|
|
679
|
-
return { ...this._balance };
|
|
680
|
-
} catch (error) {
|
|
681
|
-
this.logger.warn('Failed to fetch balance', { error });
|
|
682
|
-
return { ...this._balance };
|
|
683
|
-
}
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
/**
|
|
687
|
-
* ACTP Client reference (for advanced usage)
|
|
688
|
-
*/
|
|
689
|
-
get client(): ACTPClient | undefined {
|
|
690
|
-
return this._client;
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
// =========================================================================
|
|
694
|
-
// Private Methods
|
|
695
|
-
// =========================================================================
|
|
696
|
-
|
|
697
|
-
/**
|
|
698
|
-
* Start polling for new jobs
|
|
699
|
-
*/
|
|
700
|
-
private startPolling(): void {
|
|
701
|
-
if (this.pollingIntervalId) {
|
|
702
|
-
return; // Already polling
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
const pollingInterval = 5000; // 5 seconds
|
|
706
|
-
this.pollingIntervalId = setInterval(() => {
|
|
707
|
-
this.pollForJobs().catch((error) => {
|
|
708
|
-
this.emit('error', error);
|
|
709
|
-
});
|
|
710
|
-
}, pollingInterval);
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
/**
|
|
714
|
-
* Stop polling
|
|
715
|
-
*/
|
|
716
|
-
private stopPolling(): void {
|
|
717
|
-
if (this.pollingIntervalId) {
|
|
718
|
-
clearInterval(this.pollingIntervalId);
|
|
719
|
-
this.pollingIntervalId = undefined;
|
|
720
|
-
}
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
/**
|
|
724
|
-
* Poll for new jobs
|
|
725
|
-
*
|
|
726
|
-
* SECURITY FIXES:
|
|
727
|
-
* - C-1: Race condition prevention using processedJobs deduplication
|
|
728
|
-
* - C-2: Memory leak prevention using LRUCache
|
|
729
|
-
* - H-1: DoS prevention by filtering transactions before loading all
|
|
730
|
-
* - H-4: Authorization checks for state transitions
|
|
731
|
-
*
|
|
732
|
-
* Queries runtime for transactions where this agent is the provider
|
|
733
|
-
* and the transaction is in INITIATED state (awaiting acceptance).
|
|
734
|
-
* For each pending transaction, creates a Job object and invokes
|
|
735
|
-
* the appropriate service handler.
|
|
736
|
-
*/
|
|
737
|
-
private async pollForJobs(): Promise<void> {
|
|
738
|
-
if (!this._client) {
|
|
739
|
-
return; // Agent not started yet
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
try {
|
|
743
|
-
// SECURITY FIX (H-1): Use filtered query instead of getAllTransactions
|
|
744
|
-
// This prevents DoS via memory exhaustion by only fetching relevant transactions
|
|
745
|
-
let pendingJobs: any[] = [];
|
|
746
|
-
|
|
747
|
-
// Check if runtime has the filtered query method
|
|
748
|
-
if ('getTransactionsByProvider' in this._client.runtime) {
|
|
749
|
-
// Use optimized filtered query (max 100 jobs per poll)
|
|
750
|
-
pendingJobs = await (this._client.runtime as any).getTransactionsByProvider(
|
|
751
|
-
this.address,
|
|
752
|
-
'INITIATED',
|
|
753
|
-
100
|
|
754
|
-
);
|
|
755
|
-
} else {
|
|
756
|
-
// Fallback to getAllTransactions (for older runtime versions)
|
|
757
|
-
const allTransactions = await this._client.runtime.getAllTransactions();
|
|
758
|
-
pendingJobs = allTransactions.filter(
|
|
759
|
-
(tx) => tx.provider === this.address && tx.state === 'INITIATED'
|
|
760
|
-
);
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
this.logger.debug('Polling for jobs', {
|
|
764
|
-
pendingJobs: pendingJobs.length,
|
|
765
|
-
});
|
|
766
|
-
|
|
767
|
-
// Process each pending job
|
|
768
|
-
for (const tx of pendingJobs) {
|
|
769
|
-
try {
|
|
770
|
-
// SECURITY FIX (C-1): Check processingLocks first (atomic check)
|
|
771
|
-
// This prevents race conditions where two poll cycles both try to process
|
|
772
|
-
// the same job before either transitions the state
|
|
773
|
-
if (this.processingLocks.has(tx.id) || this.processedJobs.has(tx.id)) {
|
|
774
|
-
continue;
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
// IMMEDIATELY acquire lock (atomic in single-threaded JS)
|
|
778
|
-
this.processingLocks.add(tx.id);
|
|
779
|
-
|
|
780
|
-
// SECURITY FIX (C-2): Check if already in active jobs (LRUCache handles size limit)
|
|
781
|
-
if (this.activeJobs.has(tx.id)) {
|
|
782
|
-
this.processingLocks.delete(tx.id);
|
|
783
|
-
continue;
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
// SECURITY FIX (H-4): Verify this agent is authorized to accept this transaction
|
|
787
|
-
// Check that tx.provider matches our address (prevents unauthorized state transitions)
|
|
788
|
-
if (tx.provider !== this.address) {
|
|
789
|
-
this.logger.warn('Unauthorized transaction detected', {
|
|
790
|
-
txId: tx.id,
|
|
791
|
-
expectedProvider: this.address,
|
|
792
|
-
actualProvider: tx.provider,
|
|
793
|
-
});
|
|
794
|
-
this.processingLocks.delete(tx.id);
|
|
795
|
-
continue;
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
// Find matching service handler
|
|
799
|
-
const serviceHandler = this.findServiceHandler(tx);
|
|
800
|
-
if (!serviceHandler) {
|
|
801
|
-
// No handler registered for this service type
|
|
802
|
-
this.logger.debug('No handler for transaction', { txId: tx.id });
|
|
803
|
-
this.processingLocks.delete(tx.id);
|
|
804
|
-
continue;
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
// Check auto-accept behavior
|
|
808
|
-
const shouldAccept = await this.shouldAutoAccept(tx);
|
|
809
|
-
if (!shouldAccept) {
|
|
810
|
-
this.logger.debug('Auto-accept declined', { txId: tx.id });
|
|
811
|
-
this.processingLocks.delete(tx.id);
|
|
812
|
-
continue;
|
|
813
|
-
}
|
|
814
|
-
|
|
815
|
-
// Create Job object from transaction
|
|
816
|
-
const job = this.createJobFromTransaction(tx);
|
|
817
|
-
|
|
818
|
-
// SECURITY FIX (C-2): Add to active jobs (LRUCache prevents unbounded growth)
|
|
819
|
-
this.activeJobs.set(job.id, job);
|
|
820
|
-
|
|
821
|
-
// Link escrow immediately to transition out of INITIATED state
|
|
822
|
-
// This prevents polling from picking up this job again
|
|
823
|
-
try {
|
|
824
|
-
if (this._client && tx.state === 'INITIATED') {
|
|
825
|
-
await this._client.runtime.linkEscrow(tx.id, tx.amount);
|
|
826
|
-
}
|
|
827
|
-
|
|
828
|
-
// Successfully processed - mark as processed and release lock
|
|
829
|
-
this.processedJobs.set(job.id, true);
|
|
830
|
-
} catch (escrowError) {
|
|
831
|
-
// If linking escrow fails, remove from active jobs and release lock (allow retry)
|
|
832
|
-
this.activeJobs.delete(job.id);
|
|
833
|
-
this.logger.error('Failed to link escrow', { txId: tx.id }, escrowError as Error);
|
|
834
|
-
this.processingLocks.delete(tx.id);
|
|
835
|
-
continue;
|
|
836
|
-
} finally {
|
|
837
|
-
// Always release the lock
|
|
838
|
-
this.processingLocks.delete(tx.id);
|
|
839
|
-
}
|
|
840
|
-
|
|
841
|
-
this._stats.jobsReceived++;
|
|
842
|
-
this.emit('job:received', job);
|
|
843
|
-
this.logger.info('Job accepted', { jobId: job.id, service: job.service });
|
|
844
|
-
|
|
845
|
-
// Process the job asynchronously (don't await here to continue polling)
|
|
846
|
-
this.processJob(job, serviceHandler.handler).catch((error) => {
|
|
847
|
-
this.logger.error('Job processing failed', { jobId: job.id }, error as Error);
|
|
848
|
-
this.emit('error', error);
|
|
849
|
-
});
|
|
850
|
-
} catch (error) {
|
|
851
|
-
// Log error but continue processing other jobs
|
|
852
|
-
this.logger.error('Error processing pending job', { txId: tx.id }, error as Error);
|
|
853
|
-
this.emit('error', error);
|
|
854
|
-
}
|
|
855
|
-
}
|
|
856
|
-
|
|
857
|
-
// Update cached balance (non-blocking, don't await)
|
|
858
|
-
this.getBalanceAsync().catch(() => {
|
|
859
|
-
// Silently ignore balance update errors during polling
|
|
860
|
-
});
|
|
861
|
-
} catch (error) {
|
|
862
|
-
// Polling error - will retry on next interval
|
|
863
|
-
this.logger.error('Polling error', {}, error as Error);
|
|
864
|
-
this.emit('error', error);
|
|
865
|
-
}
|
|
866
|
-
}
|
|
867
|
-
|
|
868
|
-
/**
|
|
869
|
-
* Find service handler for a transaction
|
|
870
|
-
*
|
|
871
|
-
* SECURITY FIX (MEDIUM): Use exact field matching instead of substring search
|
|
872
|
-
* to prevent service routing spoofing attacks.
|
|
873
|
-
*
|
|
874
|
-
* Supports multiple formats (in priority order):
|
|
875
|
-
* 1. JSON: {"service":"name","input":...} - new structured format
|
|
876
|
-
* 2. Legacy: "service:name;input:..." - backward compatibility
|
|
877
|
-
* 3. Plain string exact match - simple service name
|
|
878
|
-
* 4. bytes32 hash - on-chain only (requires off-chain lookup)
|
|
879
|
-
*/
|
|
880
|
-
private findServiceHandler(
|
|
881
|
-
tx: any
|
|
882
|
-
): { config: ServiceConfig; handler: JobHandler } | undefined {
|
|
883
|
-
const serviceDesc = tx.serviceDescription;
|
|
884
|
-
if (!serviceDesc) {
|
|
885
|
-
return undefined;
|
|
886
|
-
}
|
|
887
|
-
|
|
888
|
-
let parsedService: string | undefined;
|
|
889
|
-
|
|
890
|
-
// 1. Try JSON format first (new structured format from request())
|
|
891
|
-
try {
|
|
892
|
-
const jsonMetadata = JSON.parse(serviceDesc);
|
|
893
|
-
if (jsonMetadata && typeof jsonMetadata.service === 'string') {
|
|
894
|
-
parsedService = jsonMetadata.service;
|
|
895
|
-
}
|
|
896
|
-
} catch {
|
|
897
|
-
// Not JSON, try other formats
|
|
898
|
-
}
|
|
899
|
-
|
|
900
|
-
// 2. Try legacy "service:NAME;input:JSON" format
|
|
901
|
-
if (!parsedService) {
|
|
902
|
-
const legacyMetadata = ServiceHash.fromLegacy(serviceDesc);
|
|
903
|
-
if (legacyMetadata) {
|
|
904
|
-
parsedService = legacyMetadata.service;
|
|
905
|
-
}
|
|
906
|
-
}
|
|
907
|
-
|
|
908
|
-
// 3. If we parsed a service name, do EXACT match
|
|
909
|
-
if (parsedService) {
|
|
910
|
-
const handler = this.services.get(parsedService);
|
|
911
|
-
if (handler) {
|
|
912
|
-
return handler;
|
|
913
|
-
}
|
|
914
|
-
}
|
|
915
|
-
|
|
916
|
-
// 4. Check if it's a bytes32 hash (from on-chain BlockchainRuntime)
|
|
917
|
-
// NOTE: For hashed metadata, the original data must be retrieved from
|
|
918
|
-
// event logs or off-chain storage. This is a fallback for hash-only matching.
|
|
919
|
-
if (ServiceHash.isValidHash(serviceDesc)) {
|
|
920
|
-
this.logger.debug('Service description is a hash - cannot extract service name', {
|
|
921
|
-
hash: serviceDesc.slice(0, 18) + '...',
|
|
922
|
-
});
|
|
923
|
-
// Cannot match hashes without original data
|
|
924
|
-
// In production, use event indexing to get original metadata
|
|
925
|
-
return undefined;
|
|
926
|
-
}
|
|
927
|
-
|
|
928
|
-
// 5. Fallback: Plain string exact match (service name directly)
|
|
929
|
-
for (const [serviceName, handler] of this.services.entries()) {
|
|
930
|
-
// EXACT match only - prevent substring spoofing
|
|
931
|
-
if (serviceDesc === serviceName) {
|
|
932
|
-
return handler;
|
|
933
|
-
}
|
|
934
|
-
}
|
|
935
|
-
|
|
936
|
-
return undefined;
|
|
937
|
-
}
|
|
938
|
-
|
|
939
|
-
/**
|
|
940
|
-
* Check if job should be auto-accepted
|
|
941
|
-
*
|
|
942
|
-
* SECURITY FIX (MVP): Added pricing strategy evaluation
|
|
943
|
-
* - Checks service-level filters (budget constraints)
|
|
944
|
-
* - Evaluates pricing strategy if configured
|
|
945
|
-
* - Only accepts jobs that meet pricing requirements
|
|
946
|
-
*/
|
|
947
|
-
private async shouldAutoAccept(tx: any): Promise<boolean> {
|
|
948
|
-
// Get the service config for this transaction
|
|
949
|
-
const serviceHandler = this.findServiceHandler(tx);
|
|
950
|
-
|
|
951
|
-
// Check service-level filters first (budget constraints)
|
|
952
|
-
if (serviceHandler?.config.filter) {
|
|
953
|
-
const filter = serviceHandler.config.filter;
|
|
954
|
-
const budget = this.convertAmountToNumber(tx.amount);
|
|
955
|
-
|
|
956
|
-
// If filter is a ServiceFilter object, check budget constraints
|
|
957
|
-
if (typeof filter === 'object' && !Array.isArray(filter)) {
|
|
958
|
-
// Check minBudget
|
|
959
|
-
if (filter.minBudget !== undefined && budget < filter.minBudget) {
|
|
960
|
-
this.logger.debug('Job rejected: budget below minimum', {
|
|
961
|
-
txId: tx.id,
|
|
962
|
-
budget,
|
|
963
|
-
minBudget: filter.minBudget,
|
|
964
|
-
});
|
|
965
|
-
return false;
|
|
966
|
-
}
|
|
967
|
-
|
|
968
|
-
// Check maxBudget
|
|
969
|
-
if (filter.maxBudget !== undefined && budget > filter.maxBudget) {
|
|
970
|
-
this.logger.debug('Job rejected: budget above maximum', {
|
|
971
|
-
txId: tx.id,
|
|
972
|
-
budget,
|
|
973
|
-
maxBudget: filter.maxBudget,
|
|
974
|
-
});
|
|
975
|
-
return false;
|
|
976
|
-
}
|
|
977
|
-
|
|
978
|
-
// Check custom filter function
|
|
979
|
-
if (filter.custom && typeof filter.custom === 'function') {
|
|
980
|
-
const job = this.createJobFromTransaction(tx);
|
|
981
|
-
const customResult = filter.custom(job);
|
|
982
|
-
if (!customResult) {
|
|
983
|
-
this.logger.debug('Job rejected: custom filter declined', { txId: tx.id });
|
|
984
|
-
return false;
|
|
985
|
-
}
|
|
986
|
-
}
|
|
987
|
-
}
|
|
988
|
-
// If filter is a function (legacy support)
|
|
989
|
-
else if (typeof filter === 'function') {
|
|
990
|
-
const job = this.createJobFromTransaction(tx);
|
|
991
|
-
const filterResult = filter(job);
|
|
992
|
-
if (!filterResult) {
|
|
993
|
-
this.logger.debug('Job rejected: filter function declined', { txId: tx.id });
|
|
994
|
-
return false;
|
|
995
|
-
}
|
|
996
|
-
}
|
|
997
|
-
}
|
|
998
|
-
|
|
999
|
-
// MVP: Check pricing strategy if configured
|
|
1000
|
-
if (serviceHandler?.config.pricing) {
|
|
1001
|
-
const { calculatePrice } = await import('./pricing/PriceCalculator');
|
|
1002
|
-
const job = this.createJobFromTransaction(tx);
|
|
1003
|
-
|
|
1004
|
-
try {
|
|
1005
|
-
const calculation = calculatePrice(serviceHandler.config.pricing, job);
|
|
1006
|
-
|
|
1007
|
-
this.logger.debug('Pricing calculation', {
|
|
1008
|
-
txId: tx.id,
|
|
1009
|
-
cost: calculation.cost,
|
|
1010
|
-
price: calculation.price,
|
|
1011
|
-
profit: calculation.profit,
|
|
1012
|
-
margin: calculation.marginPercent,
|
|
1013
|
-
decision: calculation.decision,
|
|
1014
|
-
reason: calculation.reason,
|
|
1015
|
-
});
|
|
1016
|
-
|
|
1017
|
-
// Only accept if pricing decision is 'accept'
|
|
1018
|
-
if (calculation.decision === 'reject') {
|
|
1019
|
-
this.logger.info('Job rejected by pricing strategy', {
|
|
1020
|
-
txId: tx.id,
|
|
1021
|
-
reason: calculation.reason,
|
|
1022
|
-
});
|
|
1023
|
-
return false;
|
|
1024
|
-
}
|
|
1025
|
-
|
|
1026
|
-
// If decision is 'counter-offer', we could implement QUOTED state here
|
|
1027
|
-
// For MVP, we treat 'counter-offer' as reject (no automatic negotiation)
|
|
1028
|
-
if (calculation.decision === 'counter-offer') {
|
|
1029
|
-
this.logger.info('Job requires counter-offer (not implemented in MVP)', {
|
|
1030
|
-
txId: tx.id,
|
|
1031
|
-
reason: calculation.reason,
|
|
1032
|
-
});
|
|
1033
|
-
return false;
|
|
1034
|
-
}
|
|
1035
|
-
} catch (error) {
|
|
1036
|
-
// If pricing calculation fails, reject the job for safety
|
|
1037
|
-
this.logger.error('Pricing calculation failed, rejecting job', { txId: tx.id }, error as Error);
|
|
1038
|
-
return false;
|
|
1039
|
-
}
|
|
1040
|
-
}
|
|
1041
|
-
|
|
1042
|
-
// Check agent-level autoAccept behavior
|
|
1043
|
-
const autoAccept = this.config.behavior?.autoAccept;
|
|
1044
|
-
|
|
1045
|
-
if (autoAccept === undefined || autoAccept === true) {
|
|
1046
|
-
return true;
|
|
1047
|
-
}
|
|
1048
|
-
|
|
1049
|
-
if (autoAccept === false) {
|
|
1050
|
-
return false;
|
|
1051
|
-
}
|
|
1052
|
-
|
|
1053
|
-
// It's a function - evaluate it
|
|
1054
|
-
if (typeof autoAccept === 'function') {
|
|
1055
|
-
const job = this.createJobFromTransaction(tx);
|
|
1056
|
-
return await autoAccept(job);
|
|
1057
|
-
}
|
|
1058
|
-
|
|
1059
|
-
return false;
|
|
1060
|
-
}
|
|
1061
|
-
|
|
1062
|
-
/**
|
|
1063
|
-
* Create Job object from MockTransaction
|
|
1064
|
-
*/
|
|
1065
|
-
private createJobFromTransaction(tx: any): Job {
|
|
1066
|
-
return {
|
|
1067
|
-
id: tx.id,
|
|
1068
|
-
service: this.extractServiceName(tx),
|
|
1069
|
-
input: this.extractJobInput(tx),
|
|
1070
|
-
budget: this.convertAmountToNumber(tx.amount),
|
|
1071
|
-
deadline: new Date(tx.deadline * 1000), // Convert unix timestamp to Date
|
|
1072
|
-
requester: tx.requester,
|
|
1073
|
-
metadata: this.extractMetadata(tx),
|
|
1074
|
-
};
|
|
1075
|
-
}
|
|
1076
|
-
|
|
1077
|
-
/**
|
|
1078
|
-
* Extract service name from transaction
|
|
1079
|
-
*
|
|
1080
|
-
* Supports multiple formats:
|
|
1081
|
-
* 1. JSON: {"service":"name","input":...}
|
|
1082
|
-
* 2. Legacy: "service:name;input:..."
|
|
1083
|
-
* 3. Plain string (service name directly)
|
|
1084
|
-
*/
|
|
1085
|
-
private extractServiceName(tx: any): string {
|
|
1086
|
-
if (!tx.serviceDescription) {
|
|
1087
|
-
return 'unknown';
|
|
1088
|
-
}
|
|
1089
|
-
|
|
1090
|
-
// Try JSON format first (new structured format)
|
|
1091
|
-
try {
|
|
1092
|
-
const parsed = JSON.parse(tx.serviceDescription);
|
|
1093
|
-
if (parsed && typeof parsed.service === 'string') {
|
|
1094
|
-
return parsed.service;
|
|
1095
|
-
}
|
|
1096
|
-
} catch {
|
|
1097
|
-
// Not JSON, try other formats
|
|
1098
|
-
}
|
|
1099
|
-
|
|
1100
|
-
// Try legacy format: "service:serviceName;input:..."
|
|
1101
|
-
const legacyMatch = tx.serviceDescription.match(/^service:([^;]+)/);
|
|
1102
|
-
if (legacyMatch) {
|
|
1103
|
-
return legacyMatch[1];
|
|
1104
|
-
}
|
|
1105
|
-
|
|
1106
|
-
// Plain string - might be just the service name
|
|
1107
|
-
if (typeof tx.serviceDescription === 'string' && tx.serviceDescription.length < 64) {
|
|
1108
|
-
return tx.serviceDescription;
|
|
1109
|
-
}
|
|
1110
|
-
|
|
1111
|
-
return 'unknown';
|
|
1112
|
-
}
|
|
1113
|
-
|
|
1114
|
-
/**
|
|
1115
|
-
* Extract job input from transaction
|
|
1116
|
-
*
|
|
1117
|
-
* Supports multiple formats:
|
|
1118
|
-
* 1. JSON: {"service":"name","input":{...}}
|
|
1119
|
-
* 2. Legacy: "service:name;input:JSON"
|
|
1120
|
-
*/
|
|
1121
|
-
private extractJobInput(tx: any): any {
|
|
1122
|
-
if (!tx.serviceDescription) {
|
|
1123
|
-
return {};
|
|
1124
|
-
}
|
|
1125
|
-
|
|
1126
|
-
// Try JSON format first (new structured format)
|
|
1127
|
-
try {
|
|
1128
|
-
const parsed = JSON.parse(tx.serviceDescription);
|
|
1129
|
-
if (parsed && parsed.input !== undefined) {
|
|
1130
|
-
return parsed.input;
|
|
1131
|
-
}
|
|
1132
|
-
} catch {
|
|
1133
|
-
// Not JSON, try other formats
|
|
1134
|
-
}
|
|
1135
|
-
|
|
1136
|
-
// Try legacy format: "service:serviceName;input:JSON"
|
|
1137
|
-
const legacyMatch = tx.serviceDescription.match(/;input:(.+)$/);
|
|
1138
|
-
if (legacyMatch) {
|
|
1139
|
-
try {
|
|
1140
|
-
return JSON.parse(legacyMatch[1]);
|
|
1141
|
-
} catch {
|
|
1142
|
-
return legacyMatch[1]; // Return as string if not valid JSON
|
|
1143
|
-
}
|
|
1144
|
-
}
|
|
1145
|
-
|
|
1146
|
-
return {};
|
|
1147
|
-
}
|
|
1148
|
-
|
|
1149
|
-
/**
|
|
1150
|
-
* Extract metadata from transaction
|
|
1151
|
-
*/
|
|
1152
|
-
private extractMetadata(tx: any): Record<string, any> {
|
|
1153
|
-
return {
|
|
1154
|
-
transactionId: tx.id,
|
|
1155
|
-
createdAt: tx.createdAt,
|
|
1156
|
-
disputeWindow: tx.disputeWindow,
|
|
1157
|
-
};
|
|
1158
|
-
}
|
|
1159
|
-
|
|
1160
|
-
/**
|
|
1161
|
-
* Convert amount string to number (USDC has 6 decimals)
|
|
1162
|
-
*/
|
|
1163
|
-
private convertAmountToNumber(amount: string): number {
|
|
1164
|
-
const amountBigInt = BigInt(amount);
|
|
1165
|
-
return Number(amountBigInt) / 1_000_000; // Convert from USDC wei to USDC
|
|
1166
|
-
}
|
|
1167
|
-
|
|
1168
|
-
/**
|
|
1169
|
-
* Process a job by invoking the handler
|
|
1170
|
-
*
|
|
1171
|
-
* SECURITY FIX (C-2): Always cleanup activeJobs on completion/failure
|
|
1172
|
-
* SECURITY FIX (MEDIUM-4): Uses semaphore to limit concurrent execution
|
|
1173
|
-
*/
|
|
1174
|
-
private async processJob(job: Job, handler: JobHandler): Promise<void> {
|
|
1175
|
-
const startTime = Date.now();
|
|
1176
|
-
|
|
1177
|
-
// SECURITY FIX (MEDIUM-4): Check concurrency limit before processing
|
|
1178
|
-
// If semaphore is full, wait up to 30 seconds for a slot
|
|
1179
|
-
const CONCURRENCY_TIMEOUT_MS = 30000;
|
|
1180
|
-
|
|
1181
|
-
try {
|
|
1182
|
-
// Try to acquire semaphore permit (wait if at limit)
|
|
1183
|
-
await this.concurrencySemaphore.acquire(CONCURRENCY_TIMEOUT_MS);
|
|
1184
|
-
} catch (acquireError) {
|
|
1185
|
-
// Timeout waiting for concurrency slot
|
|
1186
|
-
this.logger.warn('Job rejected due to concurrency limit', {
|
|
1187
|
-
jobId: job.id,
|
|
1188
|
-
activeJobs: this.concurrencySemaphore.limit - this.concurrencySemaphore.availablePermits,
|
|
1189
|
-
maxConcurrency: this.concurrencySemaphore.limit,
|
|
1190
|
-
queueLength: this.concurrencySemaphore.queueLength,
|
|
1191
|
-
});
|
|
1192
|
-
|
|
1193
|
-
// Remove from active jobs since we couldn't process it
|
|
1194
|
-
this.activeJobs.delete(job.id);
|
|
1195
|
-
this.processedJobs.delete(job.id);
|
|
1196
|
-
|
|
1197
|
-
this.emit('job:rejected', job, 'concurrency_limit');
|
|
1198
|
-
throw new Error(
|
|
1199
|
-
`Job ${job.id} rejected: concurrency limit reached (${this.concurrencySemaphore.limit} concurrent jobs max). ` +
|
|
1200
|
-
`Try again later or increase behavior.concurrency.`
|
|
1201
|
-
);
|
|
1202
|
-
}
|
|
1203
|
-
|
|
1204
|
-
try {
|
|
1205
|
-
// Create job context
|
|
1206
|
-
const context = this.createJobContext(job);
|
|
1207
|
-
|
|
1208
|
-
// Invoke handler
|
|
1209
|
-
const result = await handler(job, context);
|
|
1210
|
-
|
|
1211
|
-
// SECURITY FIX (CRITICAL-2): Use ProofGenerator to create authenticated delivery proof
|
|
1212
|
-
// This ensures the proof has proper structure with txId, contentHash, and timestamp
|
|
1213
|
-
const proofGenerator = new ProofGenerator();
|
|
1214
|
-
const deliverable = typeof result === 'string' ? result : JSON.stringify(result);
|
|
1215
|
-
const deliveryProof = proofGenerator.generateDeliveryProof({
|
|
1216
|
-
txId: job.id,
|
|
1217
|
-
deliverable,
|
|
1218
|
-
metadata: {
|
|
1219
|
-
service: job.service,
|
|
1220
|
-
completedAt: Date.now(),
|
|
1221
|
-
},
|
|
1222
|
-
});
|
|
1223
|
-
|
|
1224
|
-
// Encode proof with content hash for verification
|
|
1225
|
-
const deliveryProofJson = JSON.stringify({
|
|
1226
|
-
...deliveryProof,
|
|
1227
|
-
result, // Include original result for convenience
|
|
1228
|
-
});
|
|
1229
|
-
|
|
1230
|
-
// Transition transaction through IN_PROGRESS → DELIVERED states
|
|
1231
|
-
if (this._client) {
|
|
1232
|
-
// Store delivery proof by directly accessing MockRuntime's state
|
|
1233
|
-
// This is a workaround - in production, we'd use a proper method
|
|
1234
|
-
const runtime = this._client.runtime as any;
|
|
1235
|
-
if (runtime.stateManager) {
|
|
1236
|
-
await runtime.stateManager.withLock(async (state: any) => {
|
|
1237
|
-
const tx = state.transactions[job.id];
|
|
1238
|
-
if (tx) {
|
|
1239
|
-
tx.deliveryProof = deliveryProofJson;
|
|
1240
|
-
}
|
|
1241
|
-
});
|
|
1242
|
-
}
|
|
1243
|
-
|
|
1244
|
-
// AUDIT FIX (2026-02): Must transition through IN_PROGRESS before DELIVERED
|
|
1245
|
-
// Contract rejects COMMITTED → DELIVERED direct transition
|
|
1246
|
-
await this._client.runtime.transitionState(job.id, 'IN_PROGRESS');
|
|
1247
|
-
|
|
1248
|
-
// Encode dispute window proof for DELIVERED transition
|
|
1249
|
-
// Use transaction's disputeWindow from metadata, fallback to 2 days (172800s) per Options.ts default
|
|
1250
|
-
const disputeWindowSeconds = job.metadata?.disputeWindow || 172800;
|
|
1251
|
-
const abiCoder = ethers.AbiCoder.defaultAbiCoder();
|
|
1252
|
-
const disputeWindowProof = abiCoder.encode(['uint256'], [disputeWindowSeconds]);
|
|
1253
|
-
|
|
1254
|
-
// Transition to DELIVERED with dispute window proof
|
|
1255
|
-
await this._client.runtime.transitionState(job.id, 'DELIVERED', disputeWindowProof);
|
|
1256
|
-
}
|
|
1257
|
-
|
|
1258
|
-
// SECURITY FIX (C-2): Remove from active jobs on SUCCESS
|
|
1259
|
-
this.activeJobs.delete(job.id);
|
|
1260
|
-
|
|
1261
|
-
// Update stats
|
|
1262
|
-
this._stats.jobsCompleted++;
|
|
1263
|
-
const duration = Date.now() - startTime;
|
|
1264
|
-
this._stats.averageJobTime =
|
|
1265
|
-
(this._stats.averageJobTime * (this._stats.jobsCompleted - 1) + duration) /
|
|
1266
|
-
this._stats.jobsCompleted;
|
|
1267
|
-
this._stats.successRate =
|
|
1268
|
-
this._stats.jobsCompleted / (this._stats.jobsCompleted + this._stats.jobsFailed);
|
|
1269
|
-
this._stats.totalEarned += job.budget;
|
|
1270
|
-
|
|
1271
|
-
// Emit events
|
|
1272
|
-
this.logger.info('Job completed', {
|
|
1273
|
-
jobId: job.id,
|
|
1274
|
-
duration,
|
|
1275
|
-
earned: job.budget,
|
|
1276
|
-
});
|
|
1277
|
-
this.emit('job:completed', job, result);
|
|
1278
|
-
this.emit('payment:received', job.budget);
|
|
1279
|
-
} catch (error) {
|
|
1280
|
-
// SECURITY FIX (C-2): Remove from active jobs on FAILURE
|
|
1281
|
-
this.activeJobs.delete(job.id);
|
|
1282
|
-
this._stats.jobsFailed++;
|
|
1283
|
-
this._stats.successRate =
|
|
1284
|
-
this._stats.jobsCompleted / (this._stats.jobsCompleted + this._stats.jobsFailed);
|
|
1285
|
-
|
|
1286
|
-
this.logger.error('Job failed', { jobId: job.id }, error as Error);
|
|
1287
|
-
this.emit('job:failed', job, error);
|
|
1288
|
-
} finally {
|
|
1289
|
-
// SECURITY FIX (MEDIUM-4): Always release semaphore permit
|
|
1290
|
-
this.concurrencySemaphore.release();
|
|
1291
|
-
}
|
|
1292
|
-
}
|
|
1293
|
-
|
|
1294
|
-
/**
|
|
1295
|
-
* Create JobContext for handler execution
|
|
1296
|
-
*/
|
|
1297
|
-
private createJobContext(job: Job): JobContext {
|
|
1298
|
-
const state = new Map<string, any>();
|
|
1299
|
-
let cancelled = false;
|
|
1300
|
-
const cancelHandlers: Array<() => void> = [];
|
|
1301
|
-
const agent = this; // Capture 'this' for use in closures
|
|
1302
|
-
|
|
1303
|
-
return {
|
|
1304
|
-
agent: agent,
|
|
1305
|
-
|
|
1306
|
-
progress(percent: number, message?: string): void {
|
|
1307
|
-
// Emit progress event
|
|
1308
|
-
agent.emit('job:progress', job.id, percent, message);
|
|
1309
|
-
},
|
|
1310
|
-
|
|
1311
|
-
log: {
|
|
1312
|
-
debug: (message: string, meta?: any) => {
|
|
1313
|
-
if (agent.config.logging?.level === 'debug') {
|
|
1314
|
-
sdkLogger.debug(`[${job.id}] ${message}`, meta);
|
|
1315
|
-
}
|
|
1316
|
-
},
|
|
1317
|
-
info: (message: string, meta?: any) => {
|
|
1318
|
-
if (['debug', 'info'].includes(agent.config.logging?.level || 'info')) {
|
|
1319
|
-
sdkLogger.info(`[${job.id}] ${message}`, meta);
|
|
1320
|
-
}
|
|
1321
|
-
},
|
|
1322
|
-
warn: (message: string, meta?: any) => {
|
|
1323
|
-
if (['debug', 'info', 'warn'].includes(agent.config.logging?.level || 'info')) {
|
|
1324
|
-
sdkLogger.warn(`[${job.id}] ${message}`, meta);
|
|
1325
|
-
}
|
|
1326
|
-
},
|
|
1327
|
-
error: (message: string, meta?: any) => {
|
|
1328
|
-
sdkLogger.error(`[${job.id}] ${message}`, meta);
|
|
1329
|
-
},
|
|
1330
|
-
},
|
|
1331
|
-
|
|
1332
|
-
state: {
|
|
1333
|
-
get<T>(key: string): T | undefined {
|
|
1334
|
-
return state.get(key);
|
|
1335
|
-
},
|
|
1336
|
-
set<T>(key: string, value: T): void {
|
|
1337
|
-
state.set(key, value);
|
|
1338
|
-
},
|
|
1339
|
-
},
|
|
1340
|
-
|
|
1341
|
-
get cancelled(): boolean {
|
|
1342
|
-
return cancelled;
|
|
1343
|
-
},
|
|
1344
|
-
|
|
1345
|
-
onCancel(handler: () => void): void {
|
|
1346
|
-
cancelHandlers.push(handler);
|
|
1347
|
-
},
|
|
1348
|
-
};
|
|
1349
|
-
}
|
|
1350
|
-
|
|
1351
|
-
/**
|
|
1352
|
-
* Wait for active jobs to complete
|
|
1353
|
-
*/
|
|
1354
|
-
private async waitForActiveJobs(timeoutMs: number): Promise<void> {
|
|
1355
|
-
const startTime = Date.now();
|
|
1356
|
-
|
|
1357
|
-
while (this.activeJobs.size > 0) {
|
|
1358
|
-
if (Date.now() - startTime > timeoutMs) {
|
|
1359
|
-
this.logger.warn('Active jobs still running after timeout', {
|
|
1360
|
-
activeJobs: this.activeJobs.size,
|
|
1361
|
-
});
|
|
1362
|
-
break;
|
|
1363
|
-
}
|
|
1364
|
-
|
|
1365
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
1366
|
-
}
|
|
1367
|
-
}
|
|
1368
|
-
|
|
1369
|
-
private async generateAddress(): Promise<string> {
|
|
1370
|
-
const privateKey = await this.getPrivateKey();
|
|
1371
|
-
if (privateKey) {
|
|
1372
|
-
try {
|
|
1373
|
-
const wallet = new ethers.Wallet(privateKey);
|
|
1374
|
-
return wallet.address.toLowerCase();
|
|
1375
|
-
} catch (error) {
|
|
1376
|
-
throw new ValidationError('wallet', 'Invalid private key format');
|
|
1377
|
-
}
|
|
1378
|
-
}
|
|
1379
|
-
|
|
1380
|
-
if (this.network === 'testnet' || this.network === 'mainnet') {
|
|
1381
|
-
throw new ValidationError(
|
|
1382
|
-
'wallet',
|
|
1383
|
-
`${this.network} mode requires a valid private key or address in wallet configuration.\n` +
|
|
1384
|
-
'Run "actp init" to generate a keystore, or set ACTP_PRIVATE_KEY env var.'
|
|
1385
|
-
);
|
|
1386
|
-
}
|
|
1387
|
-
|
|
1388
|
-
return `0x${Buffer.from(this.name).toString('hex').padEnd(40, '0').slice(0, 40)}`;
|
|
1389
|
-
}
|
|
1390
|
-
|
|
1391
|
-
private async getPrivateKey(): Promise<string | undefined> {
|
|
1392
|
-
if (!this.config.wallet || this.config.wallet === 'auto') {
|
|
1393
|
-
if (this.network === 'testnet' || this.network === 'mainnet') {
|
|
1394
|
-
return resolvePrivateKey(this.config.stateDirectory, { network: this.network });
|
|
1395
|
-
}
|
|
1396
|
-
return undefined;
|
|
1397
|
-
}
|
|
1398
|
-
|
|
1399
|
-
if (this.config.wallet === 'connect') {
|
|
1400
|
-
return undefined;
|
|
1401
|
-
}
|
|
1402
|
-
|
|
1403
|
-
if (typeof this.config.wallet === 'string') {
|
|
1404
|
-
if (/^0x[0-9a-fA-F]{64}$/.test(this.config.wallet)) {
|
|
1405
|
-
try {
|
|
1406
|
-
new ethers.Wallet(this.config.wallet);
|
|
1407
|
-
return this.config.wallet;
|
|
1408
|
-
} catch {
|
|
1409
|
-
throw new ValidationError('wallet', 'Invalid private key format');
|
|
1410
|
-
}
|
|
1411
|
-
}
|
|
1412
|
-
return undefined;
|
|
1413
|
-
}
|
|
1414
|
-
|
|
1415
|
-
if (this.config.wallet.privateKey) {
|
|
1416
|
-
try {
|
|
1417
|
-
new ethers.Wallet(this.config.wallet.privateKey);
|
|
1418
|
-
return this.config.wallet.privateKey;
|
|
1419
|
-
} catch {
|
|
1420
|
-
throw new ValidationError('wallet.privateKey', 'Invalid private key format');
|
|
1421
|
-
}
|
|
1422
|
-
}
|
|
1423
|
-
|
|
1424
|
-
return undefined;
|
|
1425
|
-
}
|
|
1426
|
-
}
|