@myagentroam/node 0.9.108 → 0.9.110

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.
@@ -8,6 +8,7 @@ const nodeAppVersion = typeof nodePackage.version === 'string' ? nodePackage.ver
8
8
  const runtimeCredentialCapability = () => ({
9
9
  apiVersion: 1,
10
10
  websocket: true,
11
+ fastMode: true,
11
12
  httpVersions: ['1.1', '2']
12
13
  });
13
14
  const workspaceDirectTransferCapability = () => ({
@@ -19,6 +20,10 @@ const workspaceDirectTransferCapability = () => ({
19
20
  gitCommitBlob: true,
20
21
  maxChunkBytes: 256 * 1024
21
22
  });
23
+ const terminalDirectCapability = () => ({
24
+ apiVersion: 1,
25
+ transport: true
26
+ });
22
27
  export function reportedPlatform(platform = process.platform) {
23
28
  if (platform === 'win32')
24
29
  return 'windows';
@@ -47,7 +52,8 @@ export function unavailableCapabilities() {
47
52
  openCode: { available: false },
48
53
  marAgent: { available: true, version: nodeAppVersion },
49
54
  marAgentRuntimeCredentials: runtimeCredentialCapability(),
50
- workspaceDirectTransfer: workspaceDirectTransferCapability()
55
+ workspaceDirectTransfer: workspaceDirectTransferCapability(),
56
+ terminalDirect: terminalDirectCapability()
51
57
  };
52
58
  }
53
59
  export function detectCapabilities(commands = {}) {
@@ -80,7 +86,8 @@ export function detectCapabilities(commands = {}) {
80
86
  },
81
87
  marAgent: { available: true, version: nodeAppVersion },
82
88
  marAgentRuntimeCredentials: runtimeCredentialCapability(),
83
- workspaceDirectTransfer: workspaceDirectTransferCapability()
89
+ workspaceDirectTransfer: workspaceDirectTransferCapability(),
90
+ terminalDirect: terminalDirectCapability()
84
91
  };
85
92
  }
86
93
  export async function detectCapabilitiesAsync(commands = {}) {
@@ -117,6 +124,7 @@ export async function detectCapabilitiesAsync(commands = {}) {
117
124
  },
118
125
  marAgent: { available: true, version: nodeAppVersion },
119
126
  marAgentRuntimeCredentials: runtimeCredentialCapability(),
120
- workspaceDirectTransfer: workspaceDirectTransferCapability()
127
+ workspaceDirectTransfer: workspaceDirectTransferCapability(),
128
+ terminalDirect: terminalDirectCapability()
121
129
  };
122
130
  }
package/dist/connector.js CHANGED
@@ -1133,6 +1133,7 @@ export class NodeConnector {
1133
1133
  handleNodeRequest: (envelope) => this.nodeRequests.handle(envelope),
1134
1134
  handleRuntimeCredentialResponse: (envelope) => this.runtimeCredentials.handle(envelope),
1135
1135
  handleDirectTransfer: (envelope) => this.handleDirectTransfer(envelope),
1136
+ handleTerminalDirect: (envelope) => this.handleTerminalDirect(envelope),
1136
1137
  reconcileRuns: (envelope) => this.runEventBridge.reconcile(envelope),
1137
1138
  runners: this.runners,
1138
1139
  deliverSessionChannelMessage: (envelope) => this.runnerChannelMessages.handle(envelope),
@@ -1456,6 +1457,9 @@ export class NodeConnector {
1456
1457
  return new DirectTransferService({
1457
1458
  nodeGeneration: () => this.nodeGeneration,
1458
1459
  send: (type, payload) => this.send(type, payload),
1460
+ terminalManager: this.terminalManager,
1461
+ sendTerminalReady: (directSessionId) => this.send('terminal.direct.ready', { directSessionId }),
1462
+ sendTerminalTakeover: (directSessionId) => this.send('terminal.direct.takeover', { directSessionId }),
1459
1463
  udpPortRange: () => directTransferUdpPortRange(this.config),
1460
1464
  openUpload: async (descriptor) => {
1461
1465
  if (descriptor.direction !== 'UPLOAD' || descriptor.resource.kind !== 'WORKSPACE_FILE')
@@ -1512,6 +1516,13 @@ export class NodeConnector {
1512
1516
  });
1513
1517
  return true;
1514
1518
  }
1519
+ handleTerminalDirect(envelope) {
1520
+ const command = {
1521
+ type: envelope.type,
1522
+ ...(typeof envelope.payload === 'object' && envelope.payload !== null ? envelope.payload : {})
1523
+ };
1524
+ return this.directTransferService?.handleTerminal(command) ?? false;
1525
+ }
1515
1526
  clearMissionRetry(sessionId) {
1516
1527
  const retry = this.missionRetryTimers.get(sessionId);
1517
1528
  if (retry !== undefined)
@@ -64,6 +64,7 @@ export class MarAgentRuntimeModelFactory {
64
64
  titleGeneration: configuration.titleGeneration,
65
65
  codeMode: configuration.codeMode,
66
66
  highDensityCompaction: configuration.highDensityCompaction,
67
+ ...(configuration.fastMode === true ? { fastMode: true } : {}),
67
68
  hostedWebSearch: configuration.hostedWebSearch,
68
69
  ...(imageGeneration === undefined ? {} : { imageGeneration }),
69
70
  ...(configuration.responsesTransport === undefined
@@ -107,6 +108,7 @@ export class MarAgentRuntimeModelFactory {
107
108
  }
108
109
  : {}),
109
110
  responsesEncoding: configuration.responsesEncoding ?? 'STANDARD',
111
+ codexCredential: true,
110
112
  credentialProvider: provider
111
113
  };
112
114
  });
@@ -17,7 +17,12 @@ export class MarAgentRunner extends AbstractRunner {
17
17
  this.homeDirectory = homeDirectory;
18
18
  }
19
19
  configureModels(value) {
20
- this.#models = Array.isArray(value) ? value.filter(isModelSummary) : [];
20
+ this.#models = Array.isArray(value)
21
+ ? value.flatMap((model) => {
22
+ const parsed = marAgentNodeModelSummarySchema.safeParse(model);
23
+ return parsed.success ? [parsed.data] : [];
24
+ })
25
+ : [];
21
26
  }
22
27
  setSessionResourceReleaser(release) {
23
28
  this.#releaseSessionResources = release;
@@ -439,6 +444,3 @@ export class MarAgentRunner extends AbstractRunner {
439
444
  function firstSequenceForTurn(entries, turnId) {
440
445
  return entries.find((entry) => entry.kind === 'user' ? entry.turnId === turnId : entry.event.turnId === turnId)?.sequence;
441
446
  }
442
- function isModelSummary(value) {
443
- return marAgentNodeModelSummarySchema.safeParse(value).success;
444
- }
@@ -1,4 +1,5 @@
1
1
  import { directDataControlFrameSchema, directNodeCommandSchema } from '@myagentroam/protocol';
2
+ import { TerminalDirectService } from './terminal-direct-service.js';
2
3
  import { IceUdpMuxListener as NativeIceUdpMuxListener } from 'node-datachannel';
3
4
  import { RTCPeerConnection } from 'node-datachannel/polyfill';
4
5
  const MAX_HOST_CANDIDATES = 32;
@@ -18,6 +19,7 @@ export class DirectTransferService {
18
19
  peerConfiguration;
19
20
  leaseGraceMs;
20
21
  leaseCheckMs;
22
+ terminalDirect;
21
23
  disposed = false;
22
24
  constructor(options) {
23
25
  this.options = options;
@@ -37,6 +39,17 @@ export class DirectTransferService {
37
39
  ((configuration) => new RTCPeerConnection(configuration));
38
40
  this.leaseGraceMs = options.leaseGraceMs ?? LEASE_GRACE_MS;
39
41
  this.leaseCheckMs = options.leaseCheckMs ?? LEASE_CHECK_MS;
42
+ if (options.terminalManager !== undefined)
43
+ this.terminalDirect = new TerminalDirectService(options.terminalManager, (id) => options.sendTerminalReady?.(id), (id) => options.sendTerminalTakeover?.(id), (command, peer) => {
44
+ const transport = this.transports.get(command.transportSessionId);
45
+ return (transport?.peer === peer &&
46
+ transport.command.workbenchConnectionId === command.workbenchConnectionId &&
47
+ transport.command.nodeGeneration === command.nodeGeneration &&
48
+ command.nodeGeneration === options.nodeGeneration() &&
49
+ peer.connectionState === 'connected' &&
50
+ command.browserFingerprint === fingerprint(peer.remoteDescription?.sdp) &&
51
+ command.nodeFingerprint === fingerprint(peer.localDescription?.sdp));
52
+ }, this.now);
40
53
  }
41
54
  handle(value) {
42
55
  const parsed = directNodeCommandSchema.safeParse(value);
@@ -54,7 +67,11 @@ export class DirectTransferService {
54
67
  });
55
68
  return true;
56
69
  }
70
+ handleTerminal(value) {
71
+ return this.terminalDirect?.handle(value, (id) => this.transports.get(id)?.peer) ?? false;
72
+ }
57
73
  close() {
74
+ this.terminalDirect?.close();
58
75
  for (const transfer of [...this.transfers.values()])
59
76
  void this.removeTransfer(transfer, 'DIRECT_NODE_STOPPED');
60
77
  for (const transport of [...this.transports.values()])
@@ -65,6 +82,7 @@ export class DirectTransferService {
65
82
  return;
66
83
  this.disposed = true;
67
84
  this.close();
85
+ this.terminalDirect?.dispose();
68
86
  this.iceUdpMuxListener.stop();
69
87
  }
70
88
  async dispatch(command) {
@@ -236,6 +254,12 @@ export class DirectTransferService {
236
254
  if (attached)
237
255
  return;
238
256
  attached = true;
257
+ if (channel.label.startsWith('mar-terminal-')) {
258
+ this.terminalDirect?.open(transport.command.directSessionId, channel, event.data);
259
+ if (this.terminalDirect === undefined)
260
+ channel.close();
261
+ return;
262
+ }
239
263
  void this.openChannel(transport, channel, event.data).catch((error) => {
240
264
  channel.close();
241
265
  const id = openFrameSessionId(event.data);
@@ -396,6 +420,7 @@ export class DirectTransferService {
396
420
  if (!this.transports.delete(transport.command.directSessionId))
397
421
  return;
398
422
  clearInterval(transport.leaseTimer);
423
+ this.terminalDirect?.removeTransport(transport.command.directSessionId);
399
424
  for (const transfer of [...this.transfers.values()])
400
425
  if (transfer.command.transportSessionId === transport.command.directSessionId)
401
426
  void this.removeTransfer(transfer, code ?? 'DIRECT_CONNECTION_FAILED');
@@ -2004,6 +2004,7 @@ function modelSummary(model, index) {
2004
2004
  titleGeneration: model.titleGeneration,
2005
2005
  codeMode: model.codeMode,
2006
2006
  highDensityCompaction: model.highDensityCompaction,
2007
+ ...(model.fastMode === true ? { fastMode: true } : {}),
2007
2008
  hostedWebSearch: model.hostedWebSearch,
2008
2009
  ...(model.responsesTransport === undefined
2009
2010
  ? {}
@@ -38,6 +38,9 @@ export class NodeControlMessageService {
38
38
  if (envelope.type.startsWith('direct.') &&
39
39
  this.options.handleDirectTransfer?.(envelope) === true)
40
40
  return;
41
+ if (envelope.type.startsWith('terminal.direct.') &&
42
+ this.options.handleTerminalDirect?.(envelope) === true)
43
+ return;
41
44
  if (envelope.type === 'ack')
42
45
  return;
43
46
  if (envelope.type === 'runs.reconcile') {
@@ -0,0 +1,202 @@
1
+ import { terminalDataFrameSchema, terminalDirectNodeCommandSchema, terminalDirectOpenFrameSchema } from '@myagentroam/protocol';
2
+ const MAX_SESSIONS = 128;
3
+ const MAX_BUFFERED_BYTES = 1024 * 1024;
4
+ const MAX_FRAME_BYTES = 128 * 1024;
5
+ const HEARTBEAT_GRACE_MS = 30_000;
6
+ export class TerminalDirectService {
7
+ manager;
8
+ sendReady;
9
+ sendTakeover;
10
+ verifyTransport;
11
+ sessions = new Map();
12
+ timer;
13
+ stopListening;
14
+ now;
15
+ constructor(manager, sendReady, sendTakeover, verifyTransport, now = Date.now) {
16
+ this.manager = manager;
17
+ this.sendReady = sendReady;
18
+ this.sendTakeover = sendTakeover;
19
+ this.verifyTransport = verifyTransport;
20
+ this.now = now;
21
+ this.stopListening = manager.onFrame((frame) => {
22
+ for (const session of this.sessions.values())
23
+ if (session.opened && session.command.terminalId === frame.terminalId)
24
+ this.sendFrame(session, frame);
25
+ });
26
+ this.timer = setInterval(() => {
27
+ for (const session of [...this.sessions.values()])
28
+ if (session.expiresAt <= this.now() ||
29
+ this.now() - session.lastHeartbeat > HEARTBEAT_GRACE_MS)
30
+ this.remove(session);
31
+ }, 5_000);
32
+ this.timer.unref?.();
33
+ }
34
+ handle(value, getPeer) {
35
+ const parsed = terminalDirectNodeCommandSchema.safeParse(value);
36
+ if (!parsed.success)
37
+ return false;
38
+ const command = parsed.data;
39
+ if (command.type === 'terminal.direct.prepare') {
40
+ const peer = getPeer(command.transportSessionId);
41
+ if (peer === undefined ||
42
+ !this.verifyTransport(command, peer) ||
43
+ this.sessions.size >= MAX_SESSIONS ||
44
+ this.sessions.has(command.directSessionId) ||
45
+ command.openExpiresAt <= this.now() ||
46
+ command.expiresAt <= this.now())
47
+ return true;
48
+ this.sessions.set(command.directSessionId, {
49
+ command,
50
+ peer,
51
+ opened: false,
52
+ expiresAt: command.expiresAt,
53
+ lastHeartbeat: this.now()
54
+ });
55
+ this.sendReady(command.directSessionId);
56
+ return true;
57
+ }
58
+ const session = this.sessions.get(command.directSessionId);
59
+ if (session === undefined)
60
+ return true;
61
+ if (command.type === 'terminal.direct.cancel')
62
+ this.remove(session);
63
+ else if (session.expiresAt > this.now() && command.expiresAt > this.now()) {
64
+ session.expiresAt = command.expiresAt;
65
+ session.lastHeartbeat = this.now();
66
+ }
67
+ else
68
+ this.remove(session);
69
+ return true;
70
+ }
71
+ open(transportId, channel, raw) {
72
+ if (typeof raw !== 'string' || Buffer.byteLength(raw) > 4_096) {
73
+ channel.close();
74
+ return;
75
+ }
76
+ let parsed;
77
+ try {
78
+ parsed = terminalDirectOpenFrameSchema.safeParse(JSON.parse(raw));
79
+ }
80
+ catch {
81
+ channel.close();
82
+ return;
83
+ }
84
+ const session = parsed.success ? this.sessions.get(parsed.data.directSessionId) : undefined;
85
+ if (session === undefined ||
86
+ session.channel !== undefined ||
87
+ session.opened ||
88
+ session.command.transportSessionId !== transportId ||
89
+ session.command.grant !== (parsed.success ? parsed.data.grant : undefined) ||
90
+ session.command.openExpiresAt <= this.now() ||
91
+ session.expiresAt <= this.now() ||
92
+ !this.verifyTransport(session.command, session.peer)) {
93
+ channel.close();
94
+ return;
95
+ }
96
+ session.channel = channel;
97
+ channel.onclose = () => this.remove(session);
98
+ try {
99
+ const terminal = this.manager.get(session.command.terminalId);
100
+ if (terminal.state !== 'RUNNING' ||
101
+ terminal.workspaceId !== (session.command.workspaceId ?? null))
102
+ throw new Error('TERMINAL_NOT_FOUND');
103
+ const attachment = this.manager.authorizeAttach({
104
+ terminalId: session.command.terminalId,
105
+ afterSequence: session.command.afterSequence,
106
+ readonly: session.command.readonly,
107
+ takeover: session.command.takeover
108
+ });
109
+ session.attachmentId = attachment.attachmentId;
110
+ session.opened = true;
111
+ channel.send(JSON.stringify({
112
+ type: 'ATTACHED',
113
+ terminalId: session.command.terminalId,
114
+ afterSequence: session.command.afterSequence
115
+ }));
116
+ for (const frame of attachment.frames)
117
+ this.sendFrame(session, frame);
118
+ if (!session.command.readonly && session.command.takeover) {
119
+ for (const other of [...this.sessions.values()])
120
+ if (other !== session &&
121
+ other.opened &&
122
+ !other.command.readonly &&
123
+ other.command.terminalId === session.command.terminalId)
124
+ this.remove(other, 'TERMINAL_RELAY_REPLACED');
125
+ this.sendTakeover(session.command.directSessionId);
126
+ }
127
+ channel.onmessage = (event) => this.accept(session, event.data);
128
+ }
129
+ catch {
130
+ this.remove(session, 'TERMINAL_ATTACH_FAILED');
131
+ }
132
+ }
133
+ removeTransport(transportId) {
134
+ for (const session of [...this.sessions.values()])
135
+ if (session.command.transportSessionId === transportId)
136
+ this.remove(session);
137
+ }
138
+ close() {
139
+ for (const session of [...this.sessions.values()])
140
+ this.remove(session);
141
+ }
142
+ dispose() {
143
+ this.close();
144
+ clearInterval(this.timer);
145
+ this.stopListening();
146
+ }
147
+ accept(session, raw) {
148
+ if (!session.opened ||
149
+ typeof raw !== 'string' ||
150
+ Buffer.byteLength(raw) > MAX_FRAME_BYTES ||
151
+ session.expiresAt <= this.now()) {
152
+ this.remove(session, 'TERMINAL_FRAME_REJECTED');
153
+ return;
154
+ }
155
+ try {
156
+ const frame = terminalDataFrameSchema.parse(JSON.parse(raw));
157
+ if (frame.terminalId !== session.command.terminalId ||
158
+ !['input', 'resize', 'close'].includes(frame.type) ||
159
+ session.attachmentId === undefined)
160
+ throw new Error('TERMINAL_FRAME_INVALID');
161
+ void this.manager
162
+ .acceptFrame(session.attachmentId, frame)
163
+ .catch(() => this.remove(session, 'TERMINAL_FRAME_REJECTED'));
164
+ }
165
+ catch {
166
+ this.remove(session, 'TERMINAL_FRAME_REJECTED');
167
+ }
168
+ }
169
+ sendFrame(session, frame) {
170
+ const channel = session.channel;
171
+ if (channel?.readyState !== 'open') {
172
+ this.remove(session);
173
+ return;
174
+ }
175
+ const value = JSON.stringify(frame);
176
+ if (channel.bufferedAmount + Buffer.byteLength(value) > MAX_BUFFERED_BYTES) {
177
+ this.remove(session, 'TERMINAL_BACKPRESSURE');
178
+ return;
179
+ }
180
+ try {
181
+ channel.send(value);
182
+ }
183
+ catch {
184
+ this.remove(session);
185
+ }
186
+ }
187
+ remove(session, code) {
188
+ if (!this.sessions.delete(session.command.directSessionId))
189
+ return;
190
+ if (session.attachmentId !== undefined)
191
+ this.manager.detach(session.attachmentId);
192
+ if (code !== undefined && session.channel?.readyState === 'open') {
193
+ try {
194
+ session.channel.send(JSON.stringify({ type: 'error', terminalId: session.command.terminalId, code }));
195
+ }
196
+ catch {
197
+ // A closing channel must not prevent attachment cleanup.
198
+ }
199
+ }
200
+ session.channel?.close();
201
+ }
202
+ }
@@ -26,7 +26,8 @@ export class TerminalService {
26
26
  data?.__requestUserId)
27
27
  }),
28
28
  'terminal.close': (data) => this.close(data),
29
- 'terminal.attach.authorize': (data) => this.authorizeAttach(data)
29
+ 'terminal.attach.authorize': (data) => this.authorizeAttach(data),
30
+ 'terminal.direct.preflight': (data) => this.preflightDirect(data)
30
31
  };
31
32
  }
32
33
  open(data) {
@@ -82,6 +83,18 @@ export class TerminalService {
82
83
  const attachment = this.terminalManager.authorizeAttach(data);
83
84
  return { attachment, terminal: this.terminalManager.get(attachment.terminalId) };
84
85
  }
86
+ preflightDirect(data) {
87
+ if (data === null || typeof data !== 'object')
88
+ throw new Error('TERMINAL_ATTACH_INVALID');
89
+ const input = data;
90
+ this.assertOwner(input);
91
+ const terminal = this.terminalManager.get(String(input.terminalId));
92
+ if (terminal.state !== 'RUNNING')
93
+ throw new Error('TERMINAL_CLOSED');
94
+ if (terminal.workspaceId !== (input.workspaceId ?? null))
95
+ throw new Error('TERMINAL_WORKSPACE_MISMATCH');
96
+ return { terminalId: terminal.id, workspaceId: terminal.workspaceId };
97
+ }
85
98
  assertOwner(input) {
86
99
  const owner = this.gitOwners.get(String(input.terminalId));
87
100
  if (owner !== undefined && owner.userId !== input.__requestUserId)
@@ -47,6 +47,7 @@ export class WorkbenchManifestService {
47
47
  maxSearchResults: 200
48
48
  },
49
49
  directTransfer: capabilities.workspaceDirectTransfer,
50
+ terminalDirect: capabilities.terminalDirect,
50
51
  git: {
51
52
  available: true,
52
53
  reasonCode: null,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myagentroam/node",
3
- "version": "0.9.108",
3
+ "version": "0.9.110",
4
4
  "description": "MyAgentRoam Node runtime CLI.",
5
5
  "license": "Apache-2.0 AND GPL-2.0-or-later",
6
6
  "type": "module",
@@ -30,9 +30,9 @@
30
30
  "npm": "11.12.1",
31
31
  "ws": "^8.21.3",
32
32
  "zod": "4.4.3",
33
- "@myagentroam/agent": "0.9.108",
34
- "@myagentroam/protocol": "0.9.108",
35
- "@myagentroam/code-navigation": "0.9.108"
33
+ "@myagentroam/agent": "0.9.110",
34
+ "@myagentroam/code-navigation": "0.9.110",
35
+ "@myagentroam/protocol": "0.9.110"
36
36
  },
37
37
  "devDependencies": {
38
38
  "@types/ws": "^8.18.1"