@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.
Files changed (169) hide show
  1. package/dist/ACTPClient.d.ts +18 -0
  2. package/dist/ACTPClient.d.ts.map +1 -1
  3. package/dist/ACTPClient.js +72 -23
  4. package/dist/ACTPClient.js.map +1 -1
  5. package/dist/adapters/BasicAdapter.d.ts +15 -0
  6. package/dist/adapters/BasicAdapter.d.ts.map +1 -1
  7. package/dist/adapters/BasicAdapter.js +33 -4
  8. package/dist/adapters/BasicAdapter.js.map +1 -1
  9. package/dist/adapters/StandardAdapter.d.ts +20 -3
  10. package/dist/adapters/StandardAdapter.d.ts.map +1 -1
  11. package/dist/adapters/StandardAdapter.js +90 -12
  12. package/dist/adapters/StandardAdapter.js.map +1 -1
  13. package/dist/cli/commands/publish.js +16 -4
  14. package/dist/cli/commands/publish.js.map +1 -1
  15. package/dist/cli/commands/register.js +16 -4
  16. package/dist/cli/commands/register.js.map +1 -1
  17. package/dist/cli/commands/tx.js +31 -3
  18. package/dist/cli/commands/tx.js.map +1 -1
  19. package/dist/config/networks.d.ts +10 -2
  20. package/dist/config/networks.d.ts.map +1 -1
  21. package/dist/config/networks.js +31 -22
  22. package/dist/config/networks.js.map +1 -1
  23. package/dist/level0/request.d.ts.map +1 -1
  24. package/dist/level0/request.js +2 -1
  25. package/dist/level0/request.js.map +1 -1
  26. package/dist/runtime/BlockchainRuntime.d.ts.map +1 -1
  27. package/dist/runtime/BlockchainRuntime.js +11 -5
  28. package/dist/runtime/BlockchainRuntime.js.map +1 -1
  29. package/dist/utils/IPFSClient.d.ts +3 -1
  30. package/dist/utils/IPFSClient.d.ts.map +1 -1
  31. package/dist/utils/IPFSClient.js +27 -7
  32. package/dist/utils/IPFSClient.js.map +1 -1
  33. package/dist/wallet/AutoWalletProvider.d.ts +11 -1
  34. package/dist/wallet/AutoWalletProvider.d.ts.map +1 -1
  35. package/dist/wallet/AutoWalletProvider.js +84 -19
  36. package/dist/wallet/AutoWalletProvider.js.map +1 -1
  37. package/dist/wallet/IWalletProvider.d.ts +34 -0
  38. package/dist/wallet/IWalletProvider.d.ts.map +1 -1
  39. package/dist/wallet/SmartWalletRouter.d.ts +128 -0
  40. package/dist/wallet/SmartWalletRouter.d.ts.map +1 -0
  41. package/dist/wallet/SmartWalletRouter.js +248 -0
  42. package/dist/wallet/SmartWalletRouter.js.map +1 -0
  43. package/dist/wallet/aa/DualNonceManager.d.ts +26 -1
  44. package/dist/wallet/aa/DualNonceManager.d.ts.map +1 -1
  45. package/dist/wallet/aa/DualNonceManager.js +140 -6
  46. package/dist/wallet/aa/DualNonceManager.js.map +1 -1
  47. package/package.json +3 -6
  48. package/src/ACTPClient.ts +0 -1579
  49. package/src/abi/ACTPKernel.json +0 -1356
  50. package/src/abi/AgentRegistry.json +0 -915
  51. package/src/abi/ERC20.json +0 -40
  52. package/src/abi/EscrowVault.json +0 -134
  53. package/src/abi/IdentityRegistry.json +0 -316
  54. package/src/adapters/AdapterRegistry.ts +0 -173
  55. package/src/adapters/AdapterRouter.ts +0 -416
  56. package/src/adapters/BaseAdapter.ts +0 -498
  57. package/src/adapters/BasicAdapter.ts +0 -514
  58. package/src/adapters/IAdapter.ts +0 -292
  59. package/src/adapters/StandardAdapter.ts +0 -555
  60. package/src/adapters/X402Adapter.ts +0 -731
  61. package/src/adapters/index.ts +0 -60
  62. package/src/builders/DeliveryProofBuilder.ts +0 -327
  63. package/src/builders/QuoteBuilder.ts +0 -483
  64. package/src/builders/index.ts +0 -17
  65. package/src/cli/commands/balance.ts +0 -110
  66. package/src/cli/commands/batch.ts +0 -487
  67. package/src/cli/commands/config.ts +0 -231
  68. package/src/cli/commands/deploy-check.ts +0 -364
  69. package/src/cli/commands/deploy-env.ts +0 -120
  70. package/src/cli/commands/diff.ts +0 -141
  71. package/src/cli/commands/init.ts +0 -469
  72. package/src/cli/commands/mint.ts +0 -116
  73. package/src/cli/commands/pay.ts +0 -113
  74. package/src/cli/commands/publish.ts +0 -475
  75. package/src/cli/commands/pull.ts +0 -124
  76. package/src/cli/commands/register.ts +0 -247
  77. package/src/cli/commands/simulate.ts +0 -345
  78. package/src/cli/commands/time.ts +0 -302
  79. package/src/cli/commands/tx.ts +0 -448
  80. package/src/cli/commands/watch.ts +0 -211
  81. package/src/cli/index.ts +0 -134
  82. package/src/cli/utils/client.ts +0 -252
  83. package/src/cli/utils/config.ts +0 -389
  84. package/src/cli/utils/output.ts +0 -465
  85. package/src/cli/utils/wallet.ts +0 -109
  86. package/src/config/agirailsmd.ts +0 -262
  87. package/src/config/networks.ts +0 -275
  88. package/src/config/pendingPublish.ts +0 -237
  89. package/src/config/publishPipeline.ts +0 -359
  90. package/src/config/syncOperations.ts +0 -279
  91. package/src/erc8004/ERC8004Bridge.ts +0 -462
  92. package/src/erc8004/ReputationReporter.ts +0 -468
  93. package/src/erc8004/index.ts +0 -61
  94. package/src/errors/index.ts +0 -427
  95. package/src/index.ts +0 -364
  96. package/src/level0/Provider.ts +0 -117
  97. package/src/level0/ServiceDirectory.ts +0 -131
  98. package/src/level0/index.ts +0 -10
  99. package/src/level0/provide.ts +0 -132
  100. package/src/level0/request.ts +0 -432
  101. package/src/level1/Agent.ts +0 -1426
  102. package/src/level1/index.ts +0 -10
  103. package/src/level1/pricing/PriceCalculator.ts +0 -255
  104. package/src/level1/pricing/PricingStrategy.ts +0 -198
  105. package/src/level1/types/Job.ts +0 -179
  106. package/src/level1/types/Options.ts +0 -291
  107. package/src/level1/types/index.ts +0 -8
  108. package/src/protocol/ACTPKernel.ts +0 -808
  109. package/src/protocol/AgentRegistry.ts +0 -559
  110. package/src/protocol/DIDManager.ts +0 -629
  111. package/src/protocol/DIDResolver.ts +0 -554
  112. package/src/protocol/EASHelper.ts +0 -378
  113. package/src/protocol/EscrowVault.ts +0 -255
  114. package/src/protocol/EventMonitor.ts +0 -204
  115. package/src/protocol/MessageSigner.ts +0 -510
  116. package/src/protocol/ProofGenerator.ts +0 -339
  117. package/src/protocol/QuoteBuilder.ts +0 -15
  118. package/src/registry/AgentRegistryClient.ts +0 -202
  119. package/src/runtime/BlockchainRuntime.ts +0 -1015
  120. package/src/runtime/IACTPRuntime.ts +0 -306
  121. package/src/runtime/MockRuntime.ts +0 -1298
  122. package/src/runtime/MockStateManager.ts +0 -577
  123. package/src/runtime/index.ts +0 -25
  124. package/src/runtime/types/MockState.ts +0 -237
  125. package/src/storage/ArchiveBundleBuilder.ts +0 -561
  126. package/src/storage/ArweaveClient.ts +0 -946
  127. package/src/storage/FilebaseClient.ts +0 -790
  128. package/src/storage/index.ts +0 -96
  129. package/src/storage/types.ts +0 -348
  130. package/src/types/adapter.ts +0 -310
  131. package/src/types/agent.ts +0 -79
  132. package/src/types/did.ts +0 -223
  133. package/src/types/eip712.ts +0 -175
  134. package/src/types/erc8004.ts +0 -293
  135. package/src/types/escrow.ts +0 -27
  136. package/src/types/index.ts +0 -17
  137. package/src/types/message.ts +0 -145
  138. package/src/types/state.ts +0 -87
  139. package/src/types/transaction.ts +0 -69
  140. package/src/types/x402.ts +0 -251
  141. package/src/utils/ErrorRecoveryGuide.ts +0 -676
  142. package/src/utils/Helpers.ts +0 -688
  143. package/src/utils/IPFSClient.ts +0 -368
  144. package/src/utils/Logger.ts +0 -484
  145. package/src/utils/NonceManager.ts +0 -591
  146. package/src/utils/RateLimiter.ts +0 -534
  147. package/src/utils/ReceivedNonceTracker.ts +0 -567
  148. package/src/utils/SDKLifecycle.ts +0 -416
  149. package/src/utils/SecureNonce.ts +0 -78
  150. package/src/utils/Semaphore.ts +0 -276
  151. package/src/utils/UsedAttestationTracker.ts +0 -385
  152. package/src/utils/canonicalJson.ts +0 -38
  153. package/src/utils/circuitBreaker.ts +0 -324
  154. package/src/utils/computeTypeHash.ts +0 -48
  155. package/src/utils/fsSafe.ts +0 -80
  156. package/src/utils/index.ts +0 -80
  157. package/src/utils/retry.ts +0 -364
  158. package/src/utils/security.ts +0 -418
  159. package/src/utils/validation.ts +0 -540
  160. package/src/wallet/AutoWalletProvider.ts +0 -299
  161. package/src/wallet/EOAWalletProvider.ts +0 -69
  162. package/src/wallet/IWalletProvider.ts +0 -135
  163. package/src/wallet/aa/BundlerClient.ts +0 -274
  164. package/src/wallet/aa/DualNonceManager.ts +0 -173
  165. package/src/wallet/aa/PaymasterClient.ts +0 -174
  166. package/src/wallet/aa/TransactionBatcher.ts +0 -353
  167. package/src/wallet/aa/UserOpBuilder.ts +0 -246
  168. package/src/wallet/aa/constants.ts +0 -60
  169. package/src/wallet/keystore.ts +0 -240
@@ -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
- }