vrpc 2.5.0 → 3.0.0-alpha.2

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/vrpc/VrpcAgent.js CHANGED
@@ -6,13 +6,14 @@ const { ArgumentParser } = require('argparse')
6
6
  const VrpcAdapter = require('./VrpcAdapter')
7
7
  const EventEmitter = require('events')
8
8
 
9
+ const VRPC_PROTOCOL_VERSION = 3
10
+
9
11
  /**
10
12
  * Agent capable of making existing code available to remote control by clients.
11
13
  *
12
14
  * @extends EventEmitter
13
15
  */
14
16
  class VrpcAgent extends EventEmitter {
15
-
16
17
  /**
17
18
  * Constructs an agent by parsing command line arguments
18
19
  *
@@ -41,79 +42,47 @@ class VrpcAgent extends EventEmitter {
41
42
  add_help: true,
42
43
  description: 'VRPC Node.js Agent'
43
44
  })
44
- parser.add_argument(
45
- '-a',
46
- '--agent',
47
- {
48
- help: 'Agent name',
49
- default: agent || VrpcAgent._generateAgentName()
50
- }
51
- )
52
- parser.add_argument(
53
- '-d',
54
- '--domain',
55
- {
56
- help: 'Domain name',
57
- default: domain || 'public.vrpc'
58
- }
59
- )
60
- parser.add_argument(
61
- '-t',
62
- '--token',
63
- {
64
- help: 'access token',
65
- default: token
66
- }
67
- )
68
- parser.add_argument(
69
- '-b',
70
- '--broker',
71
- {
72
- help: 'Broker url',
73
- default: broker || 'mqtts://vrpc.io:8883'
74
- }
75
- )
76
- parser.add_argument(
77
- '-u',
78
- '--username',
79
- {
80
- help: 'Username',
81
- default: username
82
- }
83
- )
84
- parser.add_argument(
85
- '-P',
86
- '--password',
87
- {
88
- help: 'Password',
89
- default: password
90
- }
91
- )
92
- parser.add_argument(
93
- '--bestEffort',
94
- {
95
- help: 'Calls function on best-effort. Improves performance but may fail under unstable network connections.',
96
- action: 'store_true'
97
- }
98
- )
99
- parser.add_argument(
100
- '-V', '-v',
101
- '--userVersion',
102
- {
103
- help: 'User defined agent version. May be checked on the remote side for compatibility checks.',
104
- required: false,
105
- default: version,
106
- dest: 'version'
107
- }
108
- )
109
- parser.add_argument(
110
- '--version',
111
- {
112
- help: 'Returns the VRPC version this client was built with.',
113
- action: 'version',
114
- version: 'v2.3.2'
115
- }
116
- )
45
+ parser.add_argument('-a', '--agent', {
46
+ help: 'Agent name',
47
+ default: agent || VrpcAgent._generateAgentName()
48
+ })
49
+ parser.add_argument('-d', '--domain', {
50
+ help: 'Domain name',
51
+ default: domain || 'public.vrpc'
52
+ })
53
+ parser.add_argument('-t', '--token', {
54
+ help: 'access token',
55
+ default: token
56
+ })
57
+ parser.add_argument('-b', '--broker', {
58
+ help: 'Broker url',
59
+ default: broker || 'mqtts://vrpc.io:8883'
60
+ })
61
+ parser.add_argument('-u', '--username', {
62
+ help: 'Username',
63
+ default: username
64
+ })
65
+ parser.add_argument('-P', '--password', {
66
+ help: 'Password',
67
+ default: password
68
+ })
69
+ parser.add_argument('--bestEffort', {
70
+ help:
71
+ 'Calls function on best-effort. Improves performance but may fail under unstable network connections.',
72
+ action: 'store_true'
73
+ })
74
+ parser.add_argument('-V', '-v', '--userVersion', {
75
+ help:
76
+ 'User defined agent version. May be checked on the remote side for compatibility checks.',
77
+ required: false,
78
+ default: version,
79
+ dest: 'version'
80
+ })
81
+ parser.add_argument('--version', {
82
+ help: 'Returns the VRPC version this client was built with.',
83
+ action: 'version',
84
+ version: 'v2.3.2'
85
+ })
117
86
  const args = parser.parse_args()
118
87
  return new VrpcAgent(args)
119
88
  }
@@ -149,8 +118,7 @@ class VrpcAgent extends EventEmitter {
149
118
  log = 'console',
150
119
  bestEffort = false,
151
120
  version = ''
152
- } = {}
153
- ) {
121
+ } = {}) {
154
122
  super()
155
123
  this._validateDomain(domain)
156
124
  this._validateAgent(agent)
@@ -172,11 +140,11 @@ class VrpcAgent extends EventEmitter {
172
140
  this._baseTopic = `${this._domain}/${this._agent}`
173
141
  VrpcAdapter.onCallback(this._handleVrpcCallback.bind(this))
174
142
  // maps clientId to instanceId
175
- this._unnamedInstances = new Map()
176
- this._namedInstances = new Map()
143
+ this._isolatedInstances = new Map()
144
+ this._sharedInstances = new Map()
177
145
 
178
146
  // Handle the internal error event in case the user forgot to implement it
179
- this.on('error', (err) => {
147
+ this.on('error', err => {
180
148
  this._log.debug(`Encountered an error: ${err.message}`)
181
149
  })
182
150
  }
@@ -197,8 +165,11 @@ class VrpcAgent extends EventEmitter {
197
165
  * rejects
198
166
  */
199
167
  async serve () {
200
- const md5 = crypto.createHash('md5')
201
- .update(this._domain + this._agent).digest('hex').substr(0, 18)
168
+ const md5 = crypto
169
+ .createHash('md5')
170
+ .update(this._domain + this._agent)
171
+ .digest('hex')
172
+ .substr(0, 20)
202
173
  let username = this._username
203
174
  let password = this._password
204
175
  if (this._token) {
@@ -210,7 +181,7 @@ class VrpcAgent extends EventEmitter {
210
181
  password,
211
182
  keepalive: 30,
212
183
  connectTimeout: 10 * 1000,
213
- clientId: `vrpca${md5}`,
184
+ clientId: `va3${md5}`,
214
185
  rejectUnauthorized: false,
215
186
  will: {
216
187
  topic: `${this._baseTopic}/__agentInfo__`,
@@ -225,10 +196,10 @@ class VrpcAgent extends EventEmitter {
225
196
  this._log.info('Connecting to MQTT server...')
226
197
  const wasEnded = await this._clearPersistedSession()
227
198
  if (wasEnded) return
228
- this._client = mqtt.connect(
229
- this._broker,
230
- { ...this._options, clean: false }
231
- )
199
+ this._client = mqtt.connect(this._broker, {
200
+ ...this._options,
201
+ clean: false
202
+ })
232
203
  this._client.on('connect', this._handleConnect.bind(this))
233
204
  this._client.on('reconnect', this._handleReconnect.bind(this))
234
205
  this._client.on('error', this._handleError.bind(this))
@@ -269,30 +240,35 @@ class VrpcAgent extends EventEmitter {
269
240
  await new Promise(resolve => this._client.end(false, {}, resolve))
270
241
  await this._clearPersistedSession()
271
242
  } catch (err) {
272
- this._log.error(
273
- err,
274
- `Problem during disconnecting agent: ${err.message}`
275
- )
243
+ this._log.error(err, `Problem during disconnecting agent: ${err.message}`)
276
244
  }
277
245
  }
278
246
 
279
247
  static _generateAgentName () {
280
248
  const { username } = os.userInfo()
281
- const pathId = crypto.createHash('md5').update(path.resolve()).digest('hex').substr(0, 4)
249
+ const pathId = crypto
250
+ .createHash('md5')
251
+ .update(path.resolve())
252
+ .digest('hex')
253
+ .substr(0, 4)
282
254
  return `${username}-${pathId}@${os.hostname()}-${os.platform()}-js`
283
255
  }
284
256
 
285
257
  _validateDomain (domain) {
286
258
  if (!domain) throw new Error('The domain must be specified')
287
259
  if (domain.match(/[+/#*]/)) {
288
- throw new Error('The domain must NOT contain any of those characters: "+", "/", "#", "*"')
260
+ throw new Error(
261
+ 'The domain must NOT contain any of those characters: "+", "/", "#", "*"'
262
+ )
289
263
  }
290
264
  }
291
265
 
292
266
  _validateAgent (agent) {
293
267
  if (!agent) throw new Error('The agent must be specified')
294
268
  if (agent.match(/[+/#*]/)) {
295
- throw new Error('The agent must NOT contain any of those characters: "+", "/", "#", "*"')
269
+ throw new Error(
270
+ 'The agent must NOT contain any of those characters: "+", "/", "#", "*"'
271
+ )
296
272
  }
297
273
  }
298
274
 
@@ -300,7 +276,8 @@ class VrpcAgent extends EventEmitter {
300
276
  return JSON.stringify({
301
277
  status,
302
278
  hostname: os.hostname(),
303
- version: this._version
279
+ version: this._version,
280
+ v: VRPC_PROTOCOL_VERSION
304
281
  })
305
282
  }
306
283
 
@@ -309,11 +286,8 @@ class VrpcAgent extends EventEmitter {
309
286
  */
310
287
  async _clearPersistedSession () {
311
288
  // Clear potentially existing persisted sessions
312
- const client = mqtt.connect(
313
- this._broker,
314
- { ...this._options, clean: true }
315
- )
316
- client.on('error', (err) => this.emit('error', err))
289
+ const client = mqtt.connect(this._broker, { ...this._options, clean: true })
290
+ client.on('error', err => this.emit('error', err))
317
291
  client.on('offline', () => this.emit('offline'))
318
292
  client.on('reconnect', () => this.emit('reconnect'))
319
293
  return new Promise(resolve => {
@@ -329,29 +303,44 @@ class VrpcAgent extends EventEmitter {
329
303
  }
330
304
 
331
305
  _mqttPublish (topic, message, options) {
332
- this._client.publish(topic, message, { qos: this._qos, ...options }, (err) => {
333
- if (err) {
334
- this._log.warn(`Could not publish MQTT message because: ${err.message}`)
306
+ this._client.publish(
307
+ topic,
308
+ message,
309
+ { qos: this._qos, ...options },
310
+ err => {
311
+ if (err) {
312
+ this._log.warn(
313
+ `Could not publish MQTT message because: ${err.message}`
314
+ )
315
+ }
335
316
  }
336
- })
317
+ )
337
318
  }
338
319
 
339
320
  _mqttSubscribe (topic, options) {
340
- this._client.subscribe(topic, { qos: this._qos, ...options }, (err, granted) => {
341
- if (err) {
342
- this._log.warn(`Could not subscribe to topic '${topic}', because: ${err.message}`)
343
- } else {
344
- if (granted.length === 0) {
345
- this._log.debug(`Already subscribed to topic '${topic}'`)
321
+ this._client.subscribe(
322
+ topic,
323
+ { qos: this._qos, ...options },
324
+ (err, granted) => {
325
+ if (err) {
326
+ this._log.warn(
327
+ `Could not subscribe to topic '${topic}', because: ${err.message}`
328
+ )
329
+ } else {
330
+ if (granted.length === 0) {
331
+ this._log.debug(`Already subscribed to topic '${topic}'`)
332
+ }
346
333
  }
347
334
  }
348
- })
335
+ )
349
336
  }
350
337
 
351
338
  _mqttUnsubscribe (topic, options) {
352
- this._client.unsubscribe(topic, options, (err) => {
339
+ this._client.unsubscribe(topic, options, err => {
353
340
  if (err) {
354
- this._log.warn(`Could not unsubscribe from topic: ${topic} because: ${err.message}`)
341
+ this._log.warn(
342
+ `Could not unsubscribe from topic: ${topic} because: ${err.message}`
343
+ )
355
344
  }
356
345
  })
357
346
  }
@@ -377,7 +366,7 @@ class VrpcAgent extends EventEmitter {
377
366
  }
378
367
 
379
368
  async _ensureConnected () {
380
- return new Promise((resolve) => {
369
+ return new Promise(resolve => {
381
370
  if (this._client.connected) {
382
371
  resolve()
383
372
  } else {
@@ -386,11 +375,11 @@ class VrpcAgent extends EventEmitter {
386
375
  })
387
376
  }
388
377
 
389
- _handleVrpcCallback (jsonString, jsonObject) {
390
- const { sender } = jsonObject
378
+ _handleVrpcCallback (json) {
379
+ const { s } = json
391
380
  try {
392
- this._log.debug(`Forwarding callback to: ${sender} with payload:`, jsonObject)
393
- this._mqttPublish(sender, jsonString)
381
+ this._log.debug(`Forwarding callback to: ${s} with payload:`, json)
382
+ this._mqttPublish(s, VrpcAgent._stringifySafely(json))
394
383
  } catch (err) {
395
384
  this._log.warn(
396
385
  err,
@@ -399,10 +388,29 @@ class VrpcAgent extends EventEmitter {
399
388
  }
400
389
  }
401
390
 
391
+ static _stringifySafely (json) {
392
+ let str
393
+ const { c, f } = json
394
+ try {
395
+ str = JSON.stringify(json)
396
+ } catch (err) {
397
+ this._log.debug(
398
+ `Failed serialization of return value for: ${c}::${f}, because: ${err.message}`
399
+ )
400
+ json.r = '__vrpc::not-serializable__'
401
+ str = JSON.stringify(json)
402
+ }
403
+ return str
404
+ }
405
+
402
406
  _handleConnect () {
403
407
  this._log.info('[OK]')
404
408
  if (this._isReconnect) {
405
409
  this._publishAgentInfoMessage()
410
+ const classes = this._getClasses()
411
+ classes.forEach(className => {
412
+ this._publishClassInfoMessage(className)
413
+ })
406
414
  this.emit('connect')
407
415
  this._isReconnect = false
408
416
  return
@@ -448,7 +456,8 @@ class VrpcAgent extends EventEmitter {
448
456
  instances: this._getInstances(className),
449
457
  memberFunctions: this._getMemberFunctions(className),
450
458
  staticFunctions: this._getStaticFunctions(className),
451
- meta: this._getMetaData(className)
459
+ meta: this._getMetaData(className),
460
+ v: VRPC_PROTOCOL_VERSION
452
461
  }
453
462
  try {
454
463
  this._mqttPublish(
@@ -486,7 +495,7 @@ class VrpcAgent extends EventEmitter {
486
495
  const json = JSON.parse(data.toString())
487
496
  this._log.debug(`Message arrived with topic: ${topic} and payload:`, json)
488
497
  const tokens = topic.split('/')
489
- const [,, className, instance, method] = tokens
498
+ const [, , className, instance, method] = tokens
490
499
 
491
500
  // Special case: clientInfo message
492
501
  if (tokens.length === 4 && tokens[3] === '__clientInfo__') {
@@ -501,142 +510,142 @@ class VrpcAgent extends EventEmitter {
501
510
  }
502
511
 
503
512
  // Prepare RPC json
504
- json.context = instance === '__static__' ? className : instance
505
- json.method = method
513
+ // SECURITY: take as much as possible params from topic structure as this
514
+ // can be authorized by the broker. Content can't be checked...
515
+ json.c = instance === '__static__' ? className : instance
516
+ json.f = method
506
517
 
507
518
  // Mutates json and adds return value
508
- VrpcAdapter._call(json)
519
+ const mustTrack = VrpcAdapter._call(json)
520
+
521
+ if (mustTrack) {
522
+ this._mqttSubscribe(`${json.s}/__clientInfo__`)
523
+ }
509
524
 
510
525
  // Intersecting life-cycle functions
511
- let publishClassInfo = false
512
526
  switch (method) {
513
- case '__create__': {
527
+ case '__createIsolated__': {
514
528
  // TODO handle instantiation errors
515
- const instanceId = json.data.r
529
+ const instanceId = json.r
516
530
  // TODO await this
517
531
  this._subscribeToMethodsOfNewInstance(className, instanceId)
518
- this._registerUnnamedInstance(instanceId, json.sender)
532
+ this._registerIsolatedInstance(instanceId, json.s)
519
533
  break
520
534
  }
521
- case '__createNamed__': {
535
+ case '__createShared__': {
522
536
  // TODO handle instantiation errors
523
- const instanceId = json.data.r
524
- if (!this._hasNamedInstance(instanceId)) {
525
- publishClassInfo = true
537
+ const instanceId = json.r
538
+ if (!this._hasSharedInstance(instanceId)) {
526
539
  this._subscribeToMethodsOfNewInstance(className, instanceId)
540
+ this._publishClassInfoMessage(className)
527
541
  }
528
- this._registerNamedInstance(instanceId, json.sender)
529
- break
530
- }
531
- case '__getNamed__': {
532
- const { data: { _1, e }, sender } = json
533
- if (!e) this._registerNamedInstance(_1, sender)
542
+ this._registerSharedInstance(instanceId, json.s)
534
543
  break
535
544
  }
536
545
  case '__delete__': {
537
- const { data: { _1 }, sender } = json
538
546
  this._unsubscribeMethodsOfDeletedInstance(className, instance)
539
- const wasNamed = this._unregisterInstance(_1, sender)
540
- if (wasNamed) publishClassInfo = true
547
+ const wasShared = this._unregisterInstance(json.a[0], json.s)
548
+ if (wasShared) this._publishClassInfoMessage(className)
541
549
  break
542
550
  }
543
551
  }
544
- let jsonString
545
- try {
546
- jsonString = JSON.stringify(json)
547
- } catch (err) {
548
- this._log.debug(`Failed serialization of return value for: ${json.context}::${json.method}, because: ${err.message}`)
549
- json.data.r = '__vrpc::not-serializable__'
550
- jsonString = JSON.stringify(json)
551
- }
552
- if (publishClassInfo && method === '__delete__') {
553
- this._publishClassInfoMessage(className)
554
- }
555
- this._mqttPublish(json.sender, jsonString)
556
- if (publishClassInfo && method === '__createNamed__') {
557
- this._publishClassInfoMessage(className)
558
- }
552
+ this._mqttPublish(json.s, VrpcAgent._stringifySafely(json))
559
553
  } catch (err) {
560
- this._log.error(err, `Problem while handling incoming message: ${err.message}`)
554
+ this._log.error(
555
+ err,
556
+ `Problem while handling incoming message: ${err.message}`
557
+ )
561
558
  }
562
559
  }
563
560
 
564
561
  _handleClientInfoMessage (topic, json) {
565
562
  // Client went offline
566
- const clientId = topic.slice(0, -9)
563
+ const clientId = topic.slice(0, -15) // /__clientInfo__ = 15
567
564
  if (json.status === 'offline') {
568
- const entry = this._unnamedInstances.get(clientId)
569
- if (entry) { // anonymous
565
+ const entry = this._isolatedInstances.get(clientId)
566
+ if (entry) {
567
+ // anonymous
570
568
  entry.forEach(instanceId => {
571
- const json = { data: { _1: instanceId }, method: '__delete__' }
569
+ const json = { f: '__delete__', a: [instanceId], r: null }
570
+ // TODO Do not mutate!
572
571
  VrpcAdapter._call(json)
573
- const { data: { r } } = json
574
- if (r) this._log.debug(`Deleted unnamed instance: ${instanceId}`)
572
+ if (json.r) {
573
+ this._log.debug(`Auto-deleted isolated instance: ${instanceId}`)
574
+ }
575
575
  })
576
576
  }
577
577
  VrpcAdapter._unregisterEventListeners(clientId)
578
578
  this._mqttUnsubscribe(`${clientId}/__clientInfo__`)
579
+ this.emit('clientGone', clientId)
579
580
  }
580
581
  }
581
582
 
582
- _registerUnnamedInstance (instanceId, clientId) {
583
- const entry = this._unnamedInstances.get(clientId)
584
- if (entry) { // already seen
583
+ _registerIsolatedInstance (instanceId, clientId) {
584
+ const entry = this._isolatedInstances.get(clientId)
585
+ if (entry) {
586
+ // already seen
585
587
  entry.add(instanceId)
586
- } else { // new instance
587
- this._unnamedInstances.set(clientId, new Set([instanceId]))
588
- if (!this._namedInstances.has(clientId)) {
588
+ } else {
589
+ // new instance
590
+ this._isolatedInstances.set(clientId, new Set([instanceId]))
591
+ if (!this._sharedInstances.has(clientId)) {
589
592
  this._mqttSubscribe(`${clientId}/__clientInfo__`)
590
593
  }
591
594
  this._log.info(`Tracking lifetime of client: ${clientId}`)
592
595
  }
593
596
  }
594
597
 
595
- _registerNamedInstance (instanceId, clientId) {
596
- const entry = this._namedInstances.get(clientId)
597
- if (entry) { // already seen
598
+ _registerSharedInstance (instanceId, clientId) {
599
+ const entry = this._sharedInstances.get(clientId)
600
+ if (entry) {
601
+ // already seen
598
602
  entry.add(instanceId)
599
- } else { // new instance
600
- this._namedInstances.set(clientId, new Set([instanceId]))
601
- if (!this._unnamedInstances.has(clientId)) {
603
+ } else {
604
+ // new instance
605
+ this._sharedInstances.set(clientId, new Set([instanceId]))
606
+ if (!this._isolatedInstances.has(clientId)) {
602
607
  this._mqttSubscribe(`${clientId}/__clientInfo__`)
603
608
  }
604
609
  this._log.debug(`Tracking lifetime of client: ${clientId}`)
605
610
  }
606
611
  }
607
612
 
608
- _hasNamedInstance (instanceId) {
609
- for (const [, instances] of this._namedInstances) {
613
+ _hasSharedInstance (instanceId) {
614
+ for (const [, instances] of this._sharedInstances) {
610
615
  if (instances.has(instanceId)) return true
611
616
  }
612
617
  return false
613
618
  }
614
619
 
615
620
  _unregisterInstance (instanceId, clientId) {
616
- const entryUnnamed = this._unnamedInstances.get(clientId)
617
- if (entryUnnamed && entryUnnamed.has(instanceId)) {
618
- entryUnnamed.delete(instanceId)
619
- if (entryUnnamed.length === 0) {
620
- this._unnamedInstances.delete(clientId)
621
+ const entryIsolated = this._isolatedInstances.get(clientId)
622
+ if (entryIsolated && entryIsolated.has(instanceId)) {
623
+ VrpcAdapter._unregisterEventListenersOfInstance(instanceId)
624
+ entryIsolated.delete(instanceId)
625
+ if (entryIsolated.length === 0) {
626
+ this._isolatedInstances.delete(clientId)
621
627
  this._mqttUnsubscribe(`${clientId}/__clientInfo__`)
622
628
  this._log.debug(`Stopped tracking lifetime of client: ${clientId}`)
623
629
  }
624
630
  return false
625
631
  }
626
632
  let found = false
627
- this._namedInstances.forEach(async (v) => {
633
+ this._sharedInstances.forEach(async v => {
628
634
  if (v.has(instanceId)) {
629
635
  found = true
636
+ VrpcAdapter._unregisterEventListenersOfInstance(instanceId)
630
637
  v.delete(instanceId)
631
638
  if (v.length === 0) {
632
- this._namedInstances.delete(clientId)
639
+ this._sharedInstances.delete(clientId)
633
640
  this._mqttUnsubscribe(`${clientId}/__clientInfo__`)
634
641
  this._log.debug(`Stopped tracking lifetime of client: ${clientId}`)
635
642
  }
636
643
  }
637
644
  })
638
645
  if (!found) {
639
- this._log.warn(`Failed un-registering not registered instance: ${instanceId}`)
646
+ this._log.warn(
647
+ `Failed un-registering not registered instance: ${instanceId}`
648
+ )
640
649
  return false
641
650
  }
642
651
  return true
@@ -685,7 +694,7 @@ class VrpcAgent extends EventEmitter {
685
694
  * @event VrpcAgent#connect
686
695
  * @type {Object}
687
696
  * @property {Boolean} sessionPresent - A session from a previous connection is already present
688
- */
697
+ */
689
698
 
690
699
  /**
691
700
  * Event 'reconnect'
@@ -693,7 +702,7 @@ class VrpcAgent extends EventEmitter {
693
702
  * Emitted when a reconnect starts.
694
703
  *
695
704
  * @event VrpcAgent#reconnect
696
- */
705
+ */
697
706
 
698
707
  /**
699
708
  * Event 'close'
@@ -736,4 +745,14 @@ class VrpcAgent extends EventEmitter {
736
745
  * @event VrpcAgent#end
737
746
  */
738
747
 
748
+ /**
749
+ * Event 'clientGone'
750
+ *
751
+ * Emitted when a VRPC client exited that before produced state (e.g. created
752
+ * an anonymous or named instance).
753
+ *
754
+ * @event VrpcAgent#clientGone
755
+ * @type {String} clientId
756
+ */
757
+
739
758
  module.exports = VrpcAgent