@myagentroam/node 0.10.24 → 0.10.26

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/NOTICE CHANGED
@@ -9,3 +9,29 @@ Author and licensing contact: aCHAOS (achaosxyz@gmail.com)
9
9
  Licensed under MyAgentRoam Enterprise Source-Available License 1.0; see
10
10
  ee/LICENSE and LICENSING.md. Independent core and third-party notices remain
11
11
  applicable; this attribution does not transfer any contributor's copyright.
12
+
13
+ Third-party supplement: http_ece@1.2.0 (Web Push encryption dependency)
14
+ The npm archive omits its license file; the upstream v1.2.0 text follows.
15
+ Source: https://github.com/martinthomson/encrypted-content-encoding/blob/0562510a30819f52424724a6fd5504becacd98a1/LICENSE
16
+
17
+ The MIT License (MIT)
18
+
19
+ Copyright (c) 2015 Martin Thomson
20
+
21
+ Permission is hereby granted, free of charge, to any person obtaining a copy
22
+ of this software and associated documentation files (the "Software"), to deal
23
+ in the Software without restriction, including without limitation the rights
24
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
25
+ copies of the Software, and to permit persons to whom the Software is
26
+ furnished to do so, subject to the following conditions:
27
+
28
+ The above copyright notice and this permission notice shall be included in all
29
+ copies or substantial portions of the Software.
30
+
31
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
32
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
33
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
34
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
35
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
36
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
37
+ SOFTWARE.
package/dist/connector.js CHANGED
@@ -310,7 +310,10 @@ export class NodeConnector {
310
310
  throw new Error('SCHEDULE_SERVICE_UNAVAILABLE');
311
311
  return this.sessionScheduleService.createFromMcp(input);
312
312
  }, (sessionId) => this.runtime.getAgentSession(sessionId)?.externalSessionId ?? sessionId);
313
- mailNotificationMcpServer = new MailNotificationMcpServer((capability) => resolveMailNotificationCapability(this.runtime, capability), (request) => this.mailNotificationControl.request(request));
313
+ mailNotificationMcpServer = new MailNotificationMcpServer((capability) => resolveMailNotificationCapability(this.runtime, capability), (request) => this.mailNotificationControl.request(request), {
314
+ supported: () => this.mailNotificationControl.supportsChannels(),
315
+ send: (request) => this.mailNotificationControl.notification(request)
316
+ });
314
317
  runnerUsageReader;
315
318
  terminalManager;
316
319
  upgradeCoordinator;
@@ -392,7 +395,7 @@ export class NodeConnector {
392
395
  executionStarted: (executionId) => this.continuousRunCoordinator.executionStarted(executionId),
393
396
  keepRunActive: (sessionId) => (this.goalRetryTimers.has(sessionId) || this.goalContinuationPending.has(sessionId)) &&
394
397
  this.goalRuntime.get(sessionId)?.status === 'active',
395
- executionTerminating: (executionId, status) => {
398
+ executionTerminating: (executionId, status, permissionStop) => {
396
399
  this.gitOperationService.executionFinished(executionId);
397
400
  const pending = this.continuousRunCoordinator.pending(executionId);
398
401
  if (pending?.goal === undefined)
@@ -401,6 +404,11 @@ export class NodeConnector {
401
404
  const progressed = this.goalExecutionProgress.delete(executionId);
402
405
  if (!this.goalGenerationCurrent(pending.sessionId, pending.goal))
403
406
  return;
407
+ if (permissionStop !== undefined) {
408
+ this.retireGoalContinuation(pending.sessionId);
409
+ this.continuousRunCoordinator.removeGoalQueue(pending.sessionId);
410
+ return;
411
+ }
404
412
  const disposition = this.goalRuntime.recordExecutionResult(pending.sessionId, {
405
413
  failed: status === 'FAILED',
406
414
  progressed: status !== 'SUCCEEDED' || progressed
@@ -597,6 +605,8 @@ export class NodeConnector {
597
605
  });
598
606
  this.capabilities = this.projectComputerUseCapability(this.capabilities);
599
607
  this.marAgentRunController = new MarAgentManagedRunController(this.marAgentClient, {
608
+ createApproval: (input) => this.runtime.createApproval(input),
609
+ publishApproval: (input) => this.publishApprovalForRun(input),
600
610
  active: this.runState.active,
601
611
  adopt: (input) => this.runtime.adoptRun(input),
602
612
  promote: (sessionId, nativeSessionId) => this.sessionIdentityService.promote(sessionId, nativeSessionId),
@@ -910,7 +920,7 @@ export class NodeConnector {
910
920
  targetId: input.workspaceId,
911
921
  catalogEntryId: 'mar-internal-mail',
912
922
  serverName: 'mar_notification',
913
- displayName: 'MAR email notification',
923
+ displayName: 'MAR notification',
914
924
  version: '1.0.0',
915
925
  enabled: true,
916
926
  runtime: { transport: 'HTTP', url: 'https://127.0.0.1.invalid/mcp', headers: {} },
@@ -1135,7 +1145,9 @@ export class NodeConnector {
1135
1145
  return undefined;
1136
1146
  const current = this.goalRuntime.get(session.id);
1137
1147
  if (current?.status === 'active') {
1138
- if (intent !== 'REPLACE_CURRENT')
1148
+ const idlePausedGoal = this.runtime.activeSessionRun(session.id) === undefined &&
1149
+ this.runtime.sessionQueue(session.id).paused;
1150
+ if (intent !== 'REPLACE_CURRENT' && !idlePausedGoal)
1139
1151
  return undefined;
1140
1152
  this.invalidateGoalContinuation(session.id);
1141
1153
  const resumed = this.goalRuntime.resume(session.id);
@@ -1236,6 +1248,7 @@ export class NodeConnector {
1236
1248
  respondUserInput: (runId, requestId, answers) => this.openCodeRunController.respondUserInput(runId, requestId, answers)
1237
1249
  });
1238
1250
  this.marAgentClient.bindControl({
1251
+ decideApproval: (input) => this.marAgentRunController.decideApproval(input),
1239
1252
  start: (envelope) => this.marAgentRunController.start(envelope),
1240
1253
  cancel: (runId, intent) => this.marAgentRunController.cancel(runId, intent),
1241
1254
  respondUserInput: (runId, requestId, answers) => this.marAgentRunController.respondUserInput(runId, requestId, answers)
@@ -1325,6 +1338,7 @@ export class NodeConnector {
1325
1338
  config: () => this.config,
1326
1339
  register: async (nodeId, credential, payload) => {
1327
1340
  await this.connectionLifecycle.register(nodeId, credential);
1341
+ this.mailNotificationControl.setFeatures(payload);
1328
1342
  this.usageOutbox?.connected(payload);
1329
1343
  },
1330
1344
  usageAck: (payload) => this.usageOutbox?.handle(createEnvelope('usage.ack', payload)),
@@ -1340,6 +1354,7 @@ export class NodeConnector {
1340
1354
  connected: (payload) => {
1341
1355
  this.usageOutbox?.connected(payload);
1342
1356
  this.connectionLifecycle.connected();
1357
+ this.mailNotificationControl.setFeatures(payload);
1343
1358
  },
1344
1359
  close: (code, reason) => this.controlChannel.close(code, reason),
1345
1360
  inspectWorkspace: (envelope) => this.workspaceCoordinator.inspect(envelope),
@@ -2092,7 +2107,7 @@ export class NodeConnector {
2092
2107
  return goalExit;
2093
2108
  }
2094
2109
  prepareGoalSession(session) {
2095
- const access = goalUnrestrictedAccess(session.runner);
2110
+ const access = goalUnrestrictedAccess(session.runner, session.access);
2096
2111
  const requiresUpdate = session.access !== access;
2097
2112
  const changed = session.runner !== 'mar-agent' && requiresUpdate;
2098
2113
  const prepared = requiresUpdate
@@ -219,7 +219,12 @@ function resolvedMcpServers(raw, secrets) {
219
219
  mar_create_scheduled_task: ['direct', 'programmatic'],
220
220
  mar_update_scheduled_task: ['direct', 'programmatic']
221
221
  }
222
- : { mar_send_email_notification: ['direct', 'programmatic'] },
222
+ : {
223
+ mar_send_email_notification: ['direct', 'programmatic'],
224
+ ...(record['notificationChannels'] === true
225
+ ? { mar_send_notification: ['direct', 'programmatic'] }
226
+ : {})
227
+ },
223
228
  transport: 'STDIO',
224
229
  command: record['command'],
225
230
  args: record['args'],
@@ -236,7 +241,8 @@ function resolvedMcpServers(raw, secrets) {
236
241
  }
237
242
  : {
238
243
  MAR_MAIL_MCP_ENDPOINT: record['endpoint'],
239
- MAR_MAIL_MCP_TOKEN: secrets[record['secretName']]
244
+ MAR_MAIL_MCP_TOKEN: secrets[record['secretName']],
245
+ MAR_NOTIFICATION_CHANNELS: record['notificationChannels'] === true ? '1' : '0'
240
246
  })
241
247
  });
242
248
  continue;
@@ -77,9 +77,12 @@ export class MarAgentConversationNormalizer {
77
77
  return [];
78
78
  if (event.type === 'tool.started')
79
79
  return this.#toolStarted(event);
80
- if (event.type === 'tool.completed')
81
- return [...this.#toolCompleted(event), ...this.#codeModeToolResults(event)];
82
- return [...this.#toolFailed(event), ...this.#codeModeToolResults(event)];
80
+ const permissionStop = permissionStopForToolEvent(event);
81
+ const outerItems = event.type === 'tool.completed' ? this.#toolCompleted(event) : this.#toolFailed(event);
82
+ return [
83
+ ...withPermissionStop(outerItems, permissionStop),
84
+ ...this.#codeModeToolResults(event)
85
+ ];
83
86
  }
84
87
  if (event.type === 'todo.updated')
85
88
  return [
@@ -351,6 +354,14 @@ export class MarAgentConversationNormalizer {
351
354
  return [];
352
355
  }
353
356
  }
357
+ function permissionStopForToolEvent(event) {
358
+ const direct = record(event)?.permissionStop;
359
+ const cell = record(record(event)?.data)?.permissionStop;
360
+ return (direct ?? cell);
361
+ }
362
+ function withPermissionStop(items, permissionStop) {
363
+ return permissionStop === undefined ? items : items.map((item) => ({ ...item, permissionStop }));
364
+ }
354
365
  function codeModeToolResults(event) {
355
366
  if ((event.type !== 'tool.completed' && event.type !== 'tool.failed') ||
356
367
  (event.toolName !== 'code_exec' && event.toolName !== 'code_exec_wait'))
@@ -76,7 +76,9 @@ export function projectMarAgentHistory(session, entries, options = {}) {
76
76
  turns.set(event.turnId, {
77
77
  ...turn,
78
78
  status: event.type === 'execution.completed'
79
- ? 'SUCCEEDED'
79
+ ? event.permissionStop === undefined
80
+ ? 'SUCCEEDED'
81
+ : 'INTERRUPTED'
80
82
  : event.type === 'execution.failed'
81
83
  ? 'FAILED'
82
84
  : 'INTERRUPTED',
@@ -90,12 +92,12 @@ export function projectMarAgentHistory(session, entries, options = {}) {
90
92
  const attachments = projected.generatedImages === undefined
91
93
  ? []
92
94
  : (options.registerGeneratedImages?.(event.sessionId, projected.generatedImages) ?? []);
93
- replace(projectionTurn, historyItem(session, projectionTurn, projected.itemId, projected.kind, projected.status, attachments.length === 0 ? projected.payload : { ...projected.payload, attachments }, at));
95
+ replace(projectionTurn, historyItem(session, projectionTurn, projected.itemId, projected.kind, projected.status, attachments.length === 0 ? projected.payload : { ...projected.payload, attachments }, at, projected.permissionStop));
94
96
  }
95
97
  }
96
98
  return [...turns.values()];
97
99
  }
98
- function historyItem(session, turn, id, kind, status, payload, at) {
100
+ function historyItem(session, turn, id, kind, status, payload, at, permissionStop) {
99
101
  return {
100
102
  id,
101
103
  sessionId: session.id,
@@ -107,6 +109,7 @@ function historyItem(session, turn, id, kind, status, payload, at) {
107
109
  kind,
108
110
  status,
109
111
  payload,
112
+ ...(permissionStop === undefined ? {} : { permissionStop }),
110
113
  startedAt: at,
111
114
  completedAt: status === 'IN_PROGRESS' ? null : at
112
115
  };
@@ -140,6 +140,7 @@ export class MarAgentHostedConversationRuntime {
140
140
  prompt: input.prompt,
141
141
  clientMessageId: input.clientMessageId,
142
142
  modelId: input.modelId,
143
+ permissionMode: 'full-access',
143
144
  images: []
144
145
  });
145
146
  }
@@ -29,6 +29,7 @@ export class MarAgentManagedRunController {
29
29
  #runtimeCreations = new Map();
30
30
  #sessionReleases = new Map();
31
31
  #questions = new Map();
32
+ #approvals = new Map();
32
33
  #normalizers = new Map();
33
34
  #resourceIdleTtlMs;
34
35
  #retainEnvironment;
@@ -202,6 +203,7 @@ export class MarAgentManagedRunController {
202
203
  ? { runtimeScopeId: payload.marAgentRuntimeScopeId }
203
204
  : {}),
204
205
  effort: typeof payload.effort === 'string' ? payload.effort : '',
206
+ ...(payload.access === undefined ? {} : { access: payload.access }),
205
207
  mode: payload.collaborationMode === 'plan' ? 'plan' : 'run',
206
208
  mcpServers,
207
209
  images: images.map((image) => ({
@@ -414,6 +416,9 @@ export class MarAgentManagedRunController {
414
416
  ...(input.clientMessageId ? { clientMessageId: input.clientMessageId } : {}),
415
417
  modelId: input.model.id,
416
418
  ...(reasoningEffort === undefined ? {} : { reasoningEffort }),
419
+ ...(input.mode === 'compact' || input.access === undefined || input.access === null
420
+ ? {}
421
+ : { permissionMode: input.access }),
417
422
  images: input.images
418
423
  };
419
424
  const metadata = input.mode === 'compact' || !runtime.automaticTitleEnabled
@@ -475,20 +480,34 @@ export class MarAgentManagedRunController {
475
480
  });
476
481
  });
477
482
  await this.host.syncTitle(session.id, input.cwd);
478
- const status = result.status === 'completed'
479
- ? 'SUCCEEDED'
480
- : result.status === 'cancelled'
481
- ? 'CANCELLED'
482
- : 'FAILED';
483
+ const permissionStop = result.status === 'completed' ? result.permissionStop : undefined;
484
+ const status = permissionStop !== undefined
485
+ ? 'INTERRUPTED'
486
+ : result.status === 'completed'
487
+ ? 'SUCCEEDED'
488
+ : result.status === 'cancelled'
489
+ ? 'CANCELLED'
490
+ : 'FAILED';
483
491
  this.#prepareRuntimeForTerminal(input.runId, runtime);
484
492
  this.host.emit(input.runId, status === 'SUCCEEDED'
485
493
  ? 'run.completed'
486
494
  : status === 'CANCELLED'
487
495
  ? 'run.cancelled'
488
- : 'run.failed', {
496
+ : status === 'FAILED'
497
+ ? 'run.failed'
498
+ : 'run.completed', {
489
499
  status,
490
500
  ...(result.errorCode ? { code: result.errorCode } : {}),
491
- ...(result.errorMessage ? { message: result.errorMessage } : {})
501
+ ...(result.errorMessage ? { message: result.errorMessage } : {}),
502
+ ...(permissionStop === undefined
503
+ ? {}
504
+ : {
505
+ permissionStop,
506
+ code: permissionStop.action === 'ask'
507
+ ? 'MAR_AGENT_PERMISSION_ASK'
508
+ : 'MAR_AGENT_PERMISSION_DENIED',
509
+ message: permissionStop.reason
510
+ })
492
511
  }, status);
493
512
  }
494
513
  catch (error) {
@@ -656,6 +675,14 @@ export class MarAgentManagedRunController {
656
675
  const runId = runtime?.activeRunId;
657
676
  return runId === undefined ? {} : this.requestQuestion(runId, request, signal);
658
677
  },
678
+ approval: this.host.createApproval && this.host.publishApproval
679
+ ? (request, signal) => {
680
+ const runId = runtime?.activeRunId;
681
+ if (runId === undefined)
682
+ return Promise.resolve('REJECTED');
683
+ return this.requestApproval(runId, request, signal);
684
+ }
685
+ : undefined,
659
686
  mcpCatalog: () => mcp.catalog(),
660
687
  callMcp: async (call, signal, context) => mcp.call(call.name, call.arguments, signal, context?.expectedTool),
661
688
  resourceIdleTtlMs: this.#resourceIdleTtlMs
@@ -829,7 +856,62 @@ export class MarAgentManagedRunController {
829
856
  cancel();
830
857
  });
831
858
  }
859
+ decideApproval(input) {
860
+ const pending = this.#approvals.get(input.approvalId);
861
+ if (!pending || pending.runId !== input.runId)
862
+ return false;
863
+ pending.cleanup();
864
+ pending.resolve(input.decision);
865
+ return true;
866
+ }
867
+ requestApproval(runId, request, signal) {
868
+ signal.throwIfAborted();
869
+ return new Promise((resolve, reject) => {
870
+ const cleanup = () => {
871
+ signal.removeEventListener('abort', cancel);
872
+ this.#approvals.delete(request.approvalId);
873
+ };
874
+ const cancel = () => {
875
+ cleanup();
876
+ if (signal.reason instanceof Error &&
877
+ 'code' in signal.reason &&
878
+ signal.reason.code === 'MAR_AGENT_APPROVAL_EXPIRED')
879
+ this.host.emit(runId, 'approval.expired', { approvalId: request.approvalId });
880
+ reject(signal.reason);
881
+ };
882
+ const input = {
883
+ runId,
884
+ approvalId: request.approvalId,
885
+ expiresAt: request.expiresAt,
886
+ payload: {
887
+ toolName: request.source.toolName,
888
+ input: request.input,
889
+ reason: request.reason,
890
+ source: request.source
891
+ }
892
+ };
893
+ try {
894
+ this.host.createApproval(input);
895
+ this.#approvals.set(request.approvalId, { runId, resolve, cleanup });
896
+ signal.addEventListener('abort', cancel, { once: true });
897
+ if (signal.aborted)
898
+ cancel();
899
+ else
900
+ this.host.publishApproval(input);
901
+ }
902
+ catch (error) {
903
+ cleanup();
904
+ reject(error);
905
+ }
906
+ });
907
+ }
832
908
  clearInteractions(runId) {
909
+ for (const pending of this.#approvals.values()) {
910
+ if (pending.runId !== runId)
911
+ continue;
912
+ pending.cleanup();
913
+ pending.resolve('REJECTED');
914
+ }
833
915
  for (const [requestId, pending] of this.#questions) {
834
916
  if (pending.runId !== runId)
835
917
  continue;
@@ -869,6 +951,7 @@ export class MarAgentManagedRunController {
869
951
  kind: item.kind,
870
952
  status: item.status,
871
953
  payload: attachments.length === 0 ? item.payload : { ...item.payload, attachments },
954
+ ...(item.permissionStop === undefined ? {} : { permissionStop: item.permissionStop }),
872
955
  ...(item.merge === undefined ? {} : { merge: item.merge })
873
956
  });
874
957
  }
@@ -106,6 +106,11 @@ export class MarAgentRunner extends AbstractRunner {
106
106
  ...(index === 0 ? { isDefault: true } : {})
107
107
  })),
108
108
  accessOptions: [
109
+ {
110
+ id: 'auto',
111
+ label: 'Auto',
112
+ description: 'MAR Agent may stop before a tool call and request confirmation. Host validation remains active and no Agent sandbox is provided.'
113
+ },
109
114
  {
110
115
  id: 'full-access',
111
116
  label: 'Full access',
@@ -188,7 +193,7 @@ export class MarAgentRunner extends AbstractRunner {
188
193
  runner: 'mar-agent',
189
194
  model: model?.id ?? '',
190
195
  effort: model?.defaultEffort ?? '',
191
- access: 'full-access'
196
+ access: 'auto'
192
197
  };
193
198
  }
194
199
  presentSession(session, capabilities, context) {
@@ -1,4 +1,5 @@
1
1
  import { randomUUID } from 'node:crypto';
2
+ import { permissionStopSchema } from '@myagentroam/protocol';
2
3
  /**
3
4
  * Deliberately process-local runtime domain. No method in this class writes
4
5
  * into Node SQLite: a Node restart loses active runs, approvals, leases and
@@ -1329,6 +1330,9 @@ export class NodeRuntimeState {
1329
1330
  kind: projected.kind,
1330
1331
  status,
1331
1332
  payload: mergeConversationItemPayload(existing?.payload, projected),
1333
+ ...(projected.permissionStop === undefined
1334
+ ? {}
1335
+ : { permissionStop: projected.permissionStop }),
1332
1336
  startedAt: existing?.startedAt ?? event.createdAt,
1333
1337
  completedAt: completedConversationItem(status) ? event.createdAt : null
1334
1338
  };
@@ -1442,11 +1446,13 @@ function conversationItemFromEvent(event) {
1442
1446
  !isRecord(payload))
1443
1447
  return undefined;
1444
1448
  const append = event.payload.append;
1449
+ const permissionStop = permissionStopSchema.safeParse(event.payload.permissionStop);
1445
1450
  return {
1446
1451
  itemId,
1447
1452
  kind,
1448
1453
  status,
1449
1454
  payload,
1455
+ ...(permissionStop.success ? { permissionStop: permissionStop.data } : {}),
1450
1456
  merge: event.payload.merge === true,
1451
1457
  ...(append === 'sections' || append === 'outputPreview' || append === 'outputSummary'
1452
1458
  ? { append }
@@ -1,7 +1,9 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { parseComposerTags, serializeComposerTag } from '@myagentroam/protocol';
3
- export function goalUnrestrictedAccess(runner) {
4
- if (runner === 'codex' || runner === 'mar-agent')
3
+ export function goalUnrestrictedAccess(runner, currentAccess) {
4
+ if (runner === 'mar-agent')
5
+ return currentAccess ?? 'full-access';
6
+ if (runner === 'codex')
5
7
  return 'full-access';
6
8
  if (runner === 'claude-code')
7
9
  return 'bypass';
@@ -1,10 +1,18 @@
1
- import { createEnvelope, mailNotificationRequestSchema, mailNotificationResponseSchema } from '@myagentroam/protocol';
1
+ import { createEnvelope, notificationChannelsRequestSchema, notificationChannelsResponseSchema, mailNotificationRequestSchema, mailNotificationResponseSchema } from '@myagentroam/protocol';
2
2
  export class MailNotificationControl {
3
3
  sendEnvelope;
4
4
  pending = new Map();
5
5
  generation = 0;
6
6
  connectedToServer = false;
7
7
  closed = false;
8
+ channelsAvailable = false;
9
+ supportsChannels() {
10
+ return this.channelsAvailable && this.connectedToServer && !this.closed;
11
+ }
12
+ setFeatures(payload) {
13
+ const features = payload;
14
+ this.channelsAvailable = features?.notificationChannels?.apiVersion === 1;
15
+ }
8
16
  constructor(sendEnvelope) {
9
17
  this.sendEnvelope = sendEnvelope;
10
18
  }
@@ -14,6 +22,7 @@ export class MailNotificationControl {
14
22
  }
15
23
  disconnected() {
16
24
  this.connectedToServer = false;
25
+ this.channelsAvailable = false;
17
26
  this.generation++;
18
27
  for (const pending of [...this.pending.values()])
19
28
  pending.reject(new Error('MAIL_DELIVERY_UNCERTAIN'));
@@ -23,11 +32,23 @@ export class MailNotificationControl {
23
32
  this.disconnected();
24
33
  }
25
34
  handle(envelope) {
26
- if (envelope.type !== 'mail.notification.response')
35
+ if (!['mail.notification.response', 'notification.channels.response'].includes(envelope.type))
27
36
  return false;
28
37
  const pending = envelope.replyTo === undefined ? undefined : this.pending.get(envelope.replyTo);
29
- if (pending === undefined || pending.generation !== this.generation)
38
+ if (pending === undefined ||
39
+ pending.generation !== this.generation ||
40
+ pending.channel !== envelope.type)
41
+ return true;
42
+ if (envelope.type === 'notification.channels.response') {
43
+ const result = notificationChannelsResponseSchema.safeParse(envelope.payload);
44
+ if (!result.success)
45
+ pending.reject(new Error('NOTIFICATION_RESPONSE_INVALID'));
46
+ else if ('error' in result.data)
47
+ pending.reject(new Error(result.data.error.code));
48
+ else
49
+ pending.resolve(result.data);
30
50
  return true;
51
+ }
31
52
  const parsed = mailNotificationResponseSchema.safeParse(envelope.payload);
32
53
  if (!parsed.success)
33
54
  pending.reject(new Error('MAIL_RESPONSE_INVALID'));
@@ -38,9 +59,22 @@ export class MailNotificationControl {
38
59
  return true;
39
60
  }
40
61
  request(input) {
62
+ return this.requestEnvelope(createEnvelope('mail.notification.send', mailNotificationRequestSchema.parse(input))).then(() => undefined);
63
+ }
64
+ notification(input) {
65
+ if (!this.supportsChannels())
66
+ return Promise.reject(new Error('NOTIFICATION_UNSUPPORTED'));
67
+ return this.requestEnvelope(createEnvelope('notification.channels.send', notificationChannelsRequestSchema.parse(input))).then((result) => {
68
+ if (result === undefined)
69
+ throw new Error('NOTIFICATION_RESPONSE_INVALID');
70
+ if (result.callId !== input.callId)
71
+ throw new Error('NOTIFICATION_RESPONSE_INVALID');
72
+ return result;
73
+ });
74
+ }
75
+ requestEnvelope(envelope) {
41
76
  if (!this.connectedToServer || this.closed)
42
77
  return Promise.reject(new Error('MAIL_SERVER_UNAVAILABLE'));
43
- const envelope = createEnvelope('mail.notification.send', mailNotificationRequestSchema.parse(input));
44
78
  return new Promise((resolve, reject) => {
45
79
  const settle = (action) => {
46
80
  const pending = this.pending.get(envelope.id);
@@ -54,7 +88,10 @@ export class MailNotificationControl {
54
88
  timer.unref();
55
89
  this.pending.set(envelope.id, {
56
90
  generation: this.generation,
57
- resolve: () => settle(resolve),
91
+ channel: envelope.type === 'mail.notification.send'
92
+ ? 'mail.notification.response'
93
+ : 'notification.channels.response',
94
+ resolve: (result) => settle(() => resolve(result)),
58
95
  reject: (error) => settle(() => reject(error)),
59
96
  timer
60
97
  });
@@ -5,19 +5,21 @@ import { createServer } from 'node:net';
5
5
  import { tmpdir } from 'node:os';
6
6
  import { join } from 'node:path';
7
7
  import { fileURLToPath } from 'node:url';
8
- import { mailNotificationRequestSchema } from '@myagentroam/protocol';
8
+ import { notificationInputSchema, notificationChannelsRequestSchema, mailNotificationRequestSchema } from '@myagentroam/protocol';
9
9
  export class MailNotificationMcpServer {
10
10
  resolve;
11
11
  send;
12
+ channels;
12
13
  capabilities = new Map();
13
14
  sockets = new Set();
14
15
  endpoint = process.platform === 'win32'
15
16
  ? `\\\\.\\pipe\\mar-mail-${process.pid}-${randomUUID()}`
16
17
  : join(tmpdir(), `mar-mail-${process.pid}-${randomUUID()}.sock`);
17
18
  listening;
18
- constructor(resolve, send) {
19
+ constructor(resolve, send, channels) {
19
20
  this.resolve = resolve;
20
21
  this.send = send;
22
+ this.channels = channels;
21
23
  }
22
24
  async installation(capability) {
23
25
  await this.listen();
@@ -27,6 +29,7 @@ export class MailNotificationMcpServer {
27
29
  return {
28
30
  installation: {
29
31
  __marMail: true,
32
+ notificationChannels: this.channels?.supported() === true,
30
33
  serverName: 'mar_notification',
31
34
  enabled: true,
32
35
  command: process.execPath,
@@ -94,6 +97,32 @@ export class MailNotificationMcpServer {
94
97
  const current = this.resolve(capability);
95
98
  if (current === undefined)
96
99
  throw new Error('MAIL_CAPABILITY_STALE');
100
+ if (request['tool'] === 'mar_send_notification') {
101
+ if (this.channels?.supported() !== true)
102
+ throw new Error('NOTIFICATION_UNSUPPORTED');
103
+ const args = request['arguments'];
104
+ if (args === null || typeof args !== 'object' || Array.isArray(args))
105
+ throw new Error('NOTIFICATION_INPUT_INVALID');
106
+ const { callId, ...input } = args;
107
+ const parsed = notificationInputSchema.safeParse(input);
108
+ if (!parsed.success)
109
+ throw new Error('NOTIFICATION_INPUT_INVALID');
110
+ const value = notificationChannelsRequestSchema.safeParse({
111
+ ...parsed.data,
112
+ ...current,
113
+ callId,
114
+ apiVersion: 1,
115
+ eventType: 'notification',
116
+ kind: 'AGENT_MESSAGE',
117
+ occurredAt: Date.now(),
118
+ sessionTitle: current.sessionId
119
+ });
120
+ if (!value.success)
121
+ throw new Error('NOTIFICATION_INPUT_INVALID');
122
+ const result = await this.channels.send(value.data);
123
+ socket.end(`${JSON.stringify({ result })}\n`);
124
+ return;
125
+ }
97
126
  if (request['tool'] !== 'mar_send_email_notification')
98
127
  throw new Error('MAIL_TOOL_INVALID');
99
128
  const args = request['arguments'];
@@ -121,7 +150,7 @@ export class MailNotificationMcpServer {
121
150
  socket.end(`${JSON.stringify({ result: { accepted: true } })}\n`);
122
151
  }
123
152
  catch (error) {
124
- const code = error instanceof Error && /^MAIL_[A-Z0-9_]+$/u.test(error.message)
153
+ const code = error instanceof Error && /^(MAIL|NOTIFICATION)_[A-Z0-9_]+$/u.test(error.message)
125
154
  ? error.message
126
155
  : 'MAIL_DELIVERY_UNCERTAIN';
127
156
  socket.end(`${JSON.stringify({ error: code })}\n`);
@@ -3,6 +3,7 @@ import { connect } from 'node:net';
3
3
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
4
4
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
5
5
  import { z } from 'zod';
6
+ import { notificationInputSchema } from '@myagentroam/protocol';
6
7
  const endpoint = process.env['MAR_MAIL_MCP_ENDPOINT'];
7
8
  const token = process.env['MAR_MAIL_MCP_TOKEN'];
8
9
  if (endpoint === undefined || token === undefined)
@@ -29,13 +30,38 @@ server.registerTool('mar_send_email_notification', {
29
30
  const value = await request({ ...args, callId: randomUUID() });
30
31
  return { content: [{ type: 'text', text: JSON.stringify(value) }] };
31
32
  });
33
+ if (process.env['MAR_NOTIFICATION_CHANNELS'] === '1') {
34
+ server.registerTool('mar_send_notification', {
35
+ description: 'Send a real notification from the current authorized Workspace execution. Defaults to push to the initiating user; email requires an explicitly authorized recipientEmail. Channels and registered Push clients are independent. Push may be suppressed while any workbench connection is open or unavailable due to system, browser, or OS settings. Acceptance is not delivery. Do not disclose secrets or send to unauthorized recipients. Each invocation is a new side effect; do not retry after an uncertain or partial result.',
36
+ inputSchema: {
37
+ channels: z
38
+ .array(z.enum(['push', 'email']))
39
+ .min(1)
40
+ .max(2)
41
+ .optional(),
42
+ title: z
43
+ .string()
44
+ .max(160)
45
+ .regex(/^[^\r\n]*$/u)
46
+ .optional(),
47
+ message: z.string().min(1).max(4000),
48
+ recipientEmail: z.string().email().max(320).optional(),
49
+ templateId: z.enum(['', 'notification-en-US', 'notification-zh-CN']).optional()
50
+ }
51
+ }, async (args) => {
52
+ const input = notificationInputSchema.parse(args);
53
+ const value = await request({ ...input, callId: randomUUID() }, 'mar_send_notification');
54
+ return { content: [{ type: 'text', text: JSON.stringify(value) }] };
55
+ });
56
+ }
32
57
  await server.connect(new StdioServerTransport());
33
- function request(args) {
58
+ function request(args, tool = 'mar_send_email_notification') {
34
59
  return new Promise((resolve, reject) => {
35
60
  const socket = connect(endpoint);
61
+ socket.setTimeout(35_000, () => socket.destroy(new Error('MAIL_DELIVERY_UNCERTAIN')));
36
62
  let body = '';
37
63
  socket.setEncoding('utf8');
38
- socket.once('connect', () => socket.write(`${JSON.stringify({ token, tool: 'mar_send_email_notification', arguments: args })}\n`));
64
+ socket.once('connect', () => socket.write(`${JSON.stringify({ token, tool, arguments: args })}\n`));
39
65
  socket.on('data', (chunk) => {
40
66
  body += chunk;
41
67
  if (body.length > 16_000)
@@ -130,9 +130,13 @@ export class NativeSessionProjectionService {
130
130
  effort: typeof session.effort === 'string' ? session.effort : fallback.effort,
131
131
  access: typeof session.access === 'string' && session.access.trim().length > 0
132
132
  ? session.access
133
- : fallback.access
133
+ : session.runner === 'mar-agent'
134
+ ? 'full-access'
135
+ : fallback.access
134
136
  };
135
- return session.runner === 'mar-agent'
137
+ return session.runner === 'mar-agent' &&
138
+ configuration.access !== 'auto' &&
139
+ configuration.access !== 'full-access'
136
140
  ? { ...configuration, access: 'full-access' }
137
141
  : configuration;
138
142
  }
@@ -1,4 +1,4 @@
1
- import { runPerformanceSchema } from '@myagentroam/protocol';
1
+ import { permissionStopSchema, runPerformanceSchema } from '@myagentroam/protocol';
2
2
  import { nodeLog } from '../operational.js';
3
3
  import { isPlainRecord, isTerminalRunStatus } from '../util/node-operation-parsers.js';
4
4
  const COALESCE_MS = 200;
@@ -122,13 +122,20 @@ export class RunEventService {
122
122
  let finalStatus;
123
123
  let manuallyInterrupted = false;
124
124
  if (terminalCurrentExecution) {
125
- this.options.executionTerminating?.(executionId, status);
125
+ const parsedStop = isPlainRecord(payload)
126
+ ? permissionStopSchema.safeParse(payload.permissionStop)
127
+ : undefined;
128
+ const permissionStop = parsedStop?.success ? parsedStop.data : undefined;
129
+ if (permissionStop !== undefined)
130
+ this.options.runtime.pauseSessionQueue(run.sessionId);
131
+ this.options.executionTerminating?.(executionId, status, permissionStop);
126
132
  this.options.state.active.delete(executionId);
127
133
  const interrupted = this.options.state.interruptRequested.delete(executionId);
128
134
  manuallyInterrupted = interrupted;
129
135
  const queue = this.options.runtime.sessionQueue(run.sessionId);
130
- const continues = (!queue.paused && queue.itemIds.length > 0) ||
131
- this.options.keepRunActive?.(run.sessionId) === true;
136
+ const continues = permissionStop === undefined &&
137
+ ((!queue.paused && queue.itemIds.length > 0) ||
138
+ this.options.keepRunActive?.(run.sessionId) === true);
132
139
  if (!continues) {
133
140
  finalStatus =
134
141
  status === 'CANCELLED' || interrupted ? 'INTERRUPTED' : status;
@@ -52,7 +52,9 @@ export class RunnerService {
52
52
  const defaults = this.database()
53
53
  .getRunnerDefaults()
54
54
  .map((configuration) => {
55
- if (configuration.runner !== 'mar-agent' || configuration.access === 'full-access')
55
+ if (configuration.runner !== 'mar-agent' ||
56
+ configuration.access === 'auto' ||
57
+ configuration.access === 'full-access')
56
58
  return configuration;
57
59
  const normalized = {
58
60
  ...configuration,
@@ -80,7 +82,9 @@ export class RunnerService {
80
82
  const parsed = runnerDefaultConfigurationSchema.safeParse(data);
81
83
  if (!parsed.success)
82
84
  throw new Error('RUNNER_DEFAULT_INVALID');
83
- if (parsed.data.runner === 'mar-agent' && parsed.data.access !== 'full-access')
85
+ if (parsed.data.runner === 'mar-agent' &&
86
+ parsed.data.access !== 'auto' &&
87
+ parsed.data.access !== 'full-access')
84
88
  throw new Error('RUNNER_DEFAULT_INVALID');
85
89
  return { defaults: this.database().saveRunnerDefaults(parsed.data) };
86
90
  }
@@ -95,7 +95,7 @@ export class SessionLifecycleService {
95
95
  ? fallback.access
96
96
  : (saved?.access ?? fallback.access)
97
97
  };
98
- if (runner.name === 'mar-agent' && configuration.access !== 'full-access')
98
+ if (runner.name === 'mar-agent' && !['auto', 'full-access'].includes(configuration.access))
99
99
  throw new Error('SESSION_INVALID');
100
100
  const session = this.options.runtime.createAgentSession({
101
101
  workspaceId: input.workspaceId,
@@ -129,7 +129,7 @@ export class SessionLifecycleService {
129
129
  (await this.options.projection.createMetadataProjection(input.sessionId));
130
130
  if (current === undefined)
131
131
  throw new Error('SESSION_NOT_FOUND');
132
- if (current.runner === 'mar-agent' && input.access !== 'full-access')
132
+ if (current.runner === 'mar-agent' && !['auto', 'full-access'].includes(input.access))
133
133
  throw new Error('SESSION_INVALID');
134
134
  const session = this.options.runtime.updateAgentSessionConfiguration({
135
135
  sessionId: current.id,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myagentroam/node",
3
- "version": "0.10.24",
3
+ "version": "0.10.26",
4
4
  "description": "MyAgentRoam Node runtime CLI.",
5
5
  "license": "Apache-2.0 AND GPL-2.0-or-later",
6
6
  "type": "module",
@@ -37,10 +37,10 @@
37
37
  "playwright": "1.62.1",
38
38
  "ws": "^8.21.3",
39
39
  "zod": "4.4.3",
40
- "@myagentroam/agent": "0.10.24",
40
+ "@myagentroam/agent": "0.10.26",
41
41
  "@myagentroam/code-navigation": "0.10.1",
42
- "@myagentroam/marctl": "0.10.3",
43
- "@myagentroam/protocol": "0.10.24"
42
+ "@myagentroam/protocol": "0.10.26",
43
+ "@myagentroam/marctl": "0.10.3"
44
44
  },
45
45
  "devDependencies": {
46
46
  "@types/ws": "^8.18.1"