@myagentroam/node 0.10.24 → 0.10.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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
@@ -910,7 +918,7 @@ export class NodeConnector {
910
918
  targetId: input.workspaceId,
911
919
  catalogEntryId: 'mar-internal-mail',
912
920
  serverName: 'mar_notification',
913
- displayName: 'MAR email notification',
921
+ displayName: 'MAR notification',
914
922
  version: '1.0.0',
915
923
  enabled: true,
916
924
  runtime: { transport: 'HTTP', url: 'https://127.0.0.1.invalid/mcp', headers: {} },
@@ -1135,7 +1143,9 @@ export class NodeConnector {
1135
1143
  return undefined;
1136
1144
  const current = this.goalRuntime.get(session.id);
1137
1145
  if (current?.status === 'active') {
1138
- if (intent !== 'REPLACE_CURRENT')
1146
+ const idlePausedGoal = this.runtime.activeSessionRun(session.id) === undefined &&
1147
+ this.runtime.sessionQueue(session.id).paused;
1148
+ if (intent !== 'REPLACE_CURRENT' && !idlePausedGoal)
1139
1149
  return undefined;
1140
1150
  this.invalidateGoalContinuation(session.id);
1141
1151
  const resumed = this.goalRuntime.resume(session.id);
@@ -1325,6 +1335,7 @@ export class NodeConnector {
1325
1335
  config: () => this.config,
1326
1336
  register: async (nodeId, credential, payload) => {
1327
1337
  await this.connectionLifecycle.register(nodeId, credential);
1338
+ this.mailNotificationControl.setFeatures(payload);
1328
1339
  this.usageOutbox?.connected(payload);
1329
1340
  },
1330
1341
  usageAck: (payload) => this.usageOutbox?.handle(createEnvelope('usage.ack', payload)),
@@ -1340,6 +1351,7 @@ export class NodeConnector {
1340
1351
  connected: (payload) => {
1341
1352
  this.usageOutbox?.connected(payload);
1342
1353
  this.connectionLifecycle.connected();
1354
+ this.mailNotificationControl.setFeatures(payload);
1343
1355
  },
1344
1356
  close: (code, reason) => this.controlChannel.close(code, reason),
1345
1357
  inspectWorkspace: (envelope) => this.workspaceCoordinator.inspect(envelope),
@@ -2092,7 +2104,7 @@ export class NodeConnector {
2092
2104
  return goalExit;
2093
2105
  }
2094
2106
  prepareGoalSession(session) {
2095
- const access = goalUnrestrictedAccess(session.runner);
2107
+ const access = goalUnrestrictedAccess(session.runner, session.access);
2096
2108
  const requiresUpdate = session.access !== access;
2097
2109
  const changed = session.runner !== 'mar-agent' && requiresUpdate;
2098
2110
  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
  }
@@ -202,6 +202,7 @@ export class MarAgentManagedRunController {
202
202
  ? { runtimeScopeId: payload.marAgentRuntimeScopeId }
203
203
  : {}),
204
204
  effort: typeof payload.effort === 'string' ? payload.effort : '',
205
+ ...(payload.access === undefined ? {} : { access: payload.access }),
205
206
  mode: payload.collaborationMode === 'plan' ? 'plan' : 'run',
206
207
  mcpServers,
207
208
  images: images.map((image) => ({
@@ -414,6 +415,9 @@ export class MarAgentManagedRunController {
414
415
  ...(input.clientMessageId ? { clientMessageId: input.clientMessageId } : {}),
415
416
  modelId: input.model.id,
416
417
  ...(reasoningEffort === undefined ? {} : { reasoningEffort }),
418
+ ...(input.mode === 'compact' || input.access === undefined || input.access === null
419
+ ? {}
420
+ : { permissionMode: input.access }),
417
421
  images: input.images
418
422
  };
419
423
  const metadata = input.mode === 'compact' || !runtime.automaticTitleEnabled
@@ -475,20 +479,34 @@ export class MarAgentManagedRunController {
475
479
  });
476
480
  });
477
481
  await this.host.syncTitle(session.id, input.cwd);
478
- const status = result.status === 'completed'
479
- ? 'SUCCEEDED'
480
- : result.status === 'cancelled'
481
- ? 'CANCELLED'
482
- : 'FAILED';
482
+ const permissionStop = result.status === 'completed' ? result.permissionStop : undefined;
483
+ const status = permissionStop !== undefined
484
+ ? 'INTERRUPTED'
485
+ : result.status === 'completed'
486
+ ? 'SUCCEEDED'
487
+ : result.status === 'cancelled'
488
+ ? 'CANCELLED'
489
+ : 'FAILED';
483
490
  this.#prepareRuntimeForTerminal(input.runId, runtime);
484
491
  this.host.emit(input.runId, status === 'SUCCEEDED'
485
492
  ? 'run.completed'
486
493
  : status === 'CANCELLED'
487
494
  ? 'run.cancelled'
488
- : 'run.failed', {
495
+ : status === 'FAILED'
496
+ ? 'run.failed'
497
+ : 'run.completed', {
489
498
  status,
490
499
  ...(result.errorCode ? { code: result.errorCode } : {}),
491
- ...(result.errorMessage ? { message: result.errorMessage } : {})
500
+ ...(result.errorMessage ? { message: result.errorMessage } : {}),
501
+ ...(permissionStop === undefined
502
+ ? {}
503
+ : {
504
+ permissionStop,
505
+ code: permissionStop.action === 'ask'
506
+ ? 'MAR_AGENT_PERMISSION_ASK'
507
+ : 'MAR_AGENT_PERMISSION_DENIED',
508
+ message: permissionStop.reason
509
+ })
492
510
  }, status);
493
511
  }
494
512
  catch (error) {
@@ -869,6 +887,7 @@ export class MarAgentManagedRunController {
869
887
  kind: item.kind,
870
888
  status: item.status,
871
889
  payload: attachments.length === 0 ? item.payload : { ...item.payload, attachments },
890
+ ...(item.permissionStop === undefined ? {} : { permissionStop: item.permissionStop }),
872
891
  ...(item.merge === undefined ? {} : { merge: item.merge })
873
892
  });
874
893
  }
@@ -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;
@@ -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.25",
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",
41
- "@myagentroam/code-navigation": "0.10.1",
40
+ "@myagentroam/agent": "0.10.25",
42
41
  "@myagentroam/marctl": "0.10.3",
43
- "@myagentroam/protocol": "0.10.24"
42
+ "@myagentroam/code-navigation": "0.10.1",
43
+ "@myagentroam/protocol": "0.10.25"
44
44
  },
45
45
  "devDependencies": {
46
46
  "@types/ws": "^8.18.1"