@myagentroam/node 0.10.23 → 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 +17 -5
- package/dist/runner/abstract-runner.js +14 -5
- package/dist/runner/mar-agent/conversation-normalizer.js +14 -3
- package/dist/runner/mar-agent/history-projection.js +6 -3
- package/dist/runner/mar-agent/hosted-conversation-runtime.js +1 -0
- package/dist/runner/mar-agent/managed-run-controller.js +26 -7
- package/dist/runner/mar-agent-runner.js +6 -1
- package/dist/runtime-state.js +6 -0
- package/dist/service/goal-runtime.js +4 -2
- package/dist/service/mail-notification-control.js +42 -5
- package/dist/service/mail-notification-mcp-server.js +32 -3
- package/dist/service/mail-notification-mcp-stdio.js +28 -2
- package/dist/service/native-session-projection-service.js +6 -2
- package/dist/service/run-event-service.js +11 -4
- package/dist/service/session-lifecycle-service.js +2 -2
- package/package.json +4 -4
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
|
|
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
|
-
|
|
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
|
|
@@ -209,14 +209,22 @@ function resolvedMcpServers(raw, secrets) {
|
|
|
209
209
|
lifecycle: 'EXECUTION',
|
|
210
210
|
origin: 'BUILTIN',
|
|
211
211
|
allowedToolCallers: record['__marGoal'] === true
|
|
212
|
-
? {
|
|
212
|
+
? {
|
|
213
|
+
mar_get_goal: ['direct', 'programmatic'],
|
|
214
|
+
mar_update_goal: ['direct', 'programmatic']
|
|
215
|
+
}
|
|
213
216
|
: record['__marScheduled'] === true
|
|
214
217
|
? {
|
|
215
218
|
mar_get_scheduled_task: ['direct', 'programmatic'],
|
|
216
|
-
mar_create_scheduled_task: ['direct'],
|
|
217
|
-
mar_update_scheduled_task: ['direct']
|
|
219
|
+
mar_create_scheduled_task: ['direct', 'programmatic'],
|
|
220
|
+
mar_update_scheduled_task: ['direct', 'programmatic']
|
|
218
221
|
}
|
|
219
|
-
: {
|
|
222
|
+
: {
|
|
223
|
+
mar_send_email_notification: ['direct', 'programmatic'],
|
|
224
|
+
...(record['notificationChannels'] === true
|
|
225
|
+
? { mar_send_notification: ['direct', 'programmatic'] }
|
|
226
|
+
: {})
|
|
227
|
+
},
|
|
220
228
|
transport: 'STDIO',
|
|
221
229
|
command: record['command'],
|
|
222
230
|
args: record['args'],
|
|
@@ -233,7 +241,8 @@ function resolvedMcpServers(raw, secrets) {
|
|
|
233
241
|
}
|
|
234
242
|
: {
|
|
235
243
|
MAR_MAIL_MCP_ENDPOINT: record['endpoint'],
|
|
236
|
-
MAR_MAIL_MCP_TOKEN: secrets[record['secretName']]
|
|
244
|
+
MAR_MAIL_MCP_TOKEN: secrets[record['secretName']],
|
|
245
|
+
MAR_NOTIFICATION_CHANNELS: record['notificationChannels'] === true ? '1' : '0'
|
|
237
246
|
})
|
|
238
247
|
});
|
|
239
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
|
-
|
|
81
|
-
|
|
82
|
-
return [
|
|
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
|
-
?
|
|
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
|
};
|
|
@@ -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
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
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
|
-
: '
|
|
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: '
|
|
196
|
+
access: 'auto'
|
|
192
197
|
};
|
|
193
198
|
}
|
|
194
199
|
presentSession(session, capabilities, context) {
|
package/dist/runtime-state.js
CHANGED
|
@@ -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 === '
|
|
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 (
|
|
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 ||
|
|
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
|
-
|
|
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 && /^
|
|
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
|
|
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
|
-
:
|
|
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
|
-
|
|
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 =
|
|
131
|
-
|
|
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' &&
|
|
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' &&
|
|
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.
|
|
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.
|
|
40
|
+
"@myagentroam/agent": "0.10.25",
|
|
41
|
+
"@myagentroam/marctl": "0.10.3",
|
|
41
42
|
"@myagentroam/code-navigation": "0.10.1",
|
|
42
|
-
"@myagentroam/
|
|
43
|
-
"@myagentroam/protocol": "0.10.23"
|
|
43
|
+
"@myagentroam/protocol": "0.10.25"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"@types/ws": "^8.18.1"
|