@myagentroam/node 0.9.67 → 0.9.68
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/capabilities.js +14 -3
- package/dist/config.js +33 -1
- package/dist/connector.js +69 -5
- package/dist/runner/mar-agent/history-projection.js +2 -1
- package/dist/service/continuous-run-coordinator.js +1 -1
- package/dist/service/direct-transfer-service.js +453 -0
- package/dist/service/mission-runtime.js +10 -0
- package/dist/service/node-connection-lifecycle-service.js +1 -0
- package/dist/service/node-control-message-service.js +3 -0
- package/dist/service/session-message-service.js +3 -0
- package/dist/service/workbench-manifest-service.js +2 -0
- package/dist/service/workspace-file-service.js +49 -0
- package/dist/service/workspace-upload-service.js +53 -3
- package/dist/service/workspace-workbench-service.js +83 -1
- package/dist/workspace.js +61 -0
- package/package.json +4 -3
package/dist/capabilities.js
CHANGED
|
@@ -10,6 +10,14 @@ const runtimeCredentialCapability = () => ({
|
|
|
10
10
|
websocket: true,
|
|
11
11
|
httpVersions: ['1.1', '2']
|
|
12
12
|
});
|
|
13
|
+
const workspaceDirectTransferCapability = () => ({
|
|
14
|
+
apiVersion: 1,
|
|
15
|
+
probe: true,
|
|
16
|
+
upload: true,
|
|
17
|
+
download: true,
|
|
18
|
+
gitCommitBlob: true,
|
|
19
|
+
maxChunkBytes: 256 * 1024
|
|
20
|
+
});
|
|
13
21
|
export function reportedPlatform(platform = process.platform) {
|
|
14
22
|
if (platform === 'win32')
|
|
15
23
|
return 'windows';
|
|
@@ -37,7 +45,8 @@ export function unavailableCapabilities() {
|
|
|
37
45
|
claudeCode: { available: false },
|
|
38
46
|
openCode: { available: false },
|
|
39
47
|
marAgent: { available: true, version: nodeAppVersion },
|
|
40
|
-
marAgentRuntimeCredentials: runtimeCredentialCapability()
|
|
48
|
+
marAgentRuntimeCredentials: runtimeCredentialCapability(),
|
|
49
|
+
workspaceDirectTransfer: workspaceDirectTransferCapability()
|
|
41
50
|
};
|
|
42
51
|
}
|
|
43
52
|
export function detectCapabilities(commands = {}) {
|
|
@@ -69,7 +78,8 @@ export function detectCapabilities(commands = {}) {
|
|
|
69
78
|
...(openCode.version === undefined ? {} : { version: openCode.version })
|
|
70
79
|
},
|
|
71
80
|
marAgent: { available: true, version: nodeAppVersion },
|
|
72
|
-
marAgentRuntimeCredentials: runtimeCredentialCapability()
|
|
81
|
+
marAgentRuntimeCredentials: runtimeCredentialCapability(),
|
|
82
|
+
workspaceDirectTransfer: workspaceDirectTransferCapability()
|
|
73
83
|
};
|
|
74
84
|
}
|
|
75
85
|
export async function detectCapabilitiesAsync(commands = {}) {
|
|
@@ -105,6 +115,7 @@ export async function detectCapabilitiesAsync(commands = {}) {
|
|
|
105
115
|
...(openCode.version === undefined ? {} : { version: openCode.version })
|
|
106
116
|
},
|
|
107
117
|
marAgent: { available: true, version: nodeAppVersion },
|
|
108
|
-
marAgentRuntimeCredentials: runtimeCredentialCapability()
|
|
118
|
+
marAgentRuntimeCredentials: runtimeCredentialCapability(),
|
|
119
|
+
workspaceDirectTransfer: workspaceDirectTransferCapability()
|
|
109
120
|
};
|
|
110
121
|
}
|
package/dist/config.js
CHANGED
|
@@ -26,6 +26,7 @@ export async function loadNodeConfig(path = nodeConfigPath()) {
|
|
|
26
26
|
? input['allowedRoots']
|
|
27
27
|
: defaultAllowedRoots();
|
|
28
28
|
const conversationHosting = parseConversationHosting(input['conversationHosting']);
|
|
29
|
+
const directTransfer = parseDirectTransfer(input['directTransfer']);
|
|
29
30
|
return {
|
|
30
31
|
serverUrl: parsed.serverUrl,
|
|
31
32
|
allowedRoots,
|
|
@@ -33,7 +34,15 @@ export async function loadNodeConfig(path = nodeConfigPath()) {
|
|
|
33
34
|
...(credential === undefined ? {} : { credential }),
|
|
34
35
|
...(registrationToken === undefined ? {} : { registrationToken }),
|
|
35
36
|
...(dataDirectory === undefined ? {} : { dataDirectory }),
|
|
36
|
-
...(conversationHosting === undefined ? {} : { conversationHosting })
|
|
37
|
+
...(conversationHosting === undefined ? {} : { conversationHosting }),
|
|
38
|
+
...(directTransfer === undefined ? {} : { directTransfer })
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
export function directTransferUdpPortRange(config) {
|
|
42
|
+
const directTransfer = config?.directTransfer;
|
|
43
|
+
return {
|
|
44
|
+
begin: directTransfer?.udpPortRangeBegin ?? 49_152,
|
|
45
|
+
end: directTransfer?.udpPortRangeEnd ?? 49_215
|
|
37
46
|
};
|
|
38
47
|
}
|
|
39
48
|
/** Uses the Node service account's scope when no Workspace roots are configured. */
|
|
@@ -97,6 +106,29 @@ function parseConversationHosting(value) {
|
|
|
97
106
|
inactiveRetentionDays: inactiveRetentionDays
|
|
98
107
|
};
|
|
99
108
|
}
|
|
109
|
+
function parseDirectTransfer(value) {
|
|
110
|
+
if (value === undefined)
|
|
111
|
+
return undefined;
|
|
112
|
+
if (typeof value !== 'object' ||
|
|
113
|
+
value === null ||
|
|
114
|
+
Array.isArray(value) ||
|
|
115
|
+
Object.keys(value).some((key) => key !== 'udpPortRangeBegin' && key !== 'udpPortRangeEnd'))
|
|
116
|
+
throw new Error('NODE_CONFIG_INVALID');
|
|
117
|
+
const input = value;
|
|
118
|
+
const begin = input['udpPortRangeBegin'];
|
|
119
|
+
const end = input['udpPortRangeEnd'];
|
|
120
|
+
if (!Number.isInteger(begin) ||
|
|
121
|
+
!Number.isInteger(end) ||
|
|
122
|
+
begin < 1_024 ||
|
|
123
|
+
end > 65_535 ||
|
|
124
|
+
begin > end ||
|
|
125
|
+
end - begin + 1 > 256)
|
|
126
|
+
throw new Error('NODE_CONFIG_INVALID');
|
|
127
|
+
return {
|
|
128
|
+
udpPortRangeBegin: begin,
|
|
129
|
+
udpPortRangeEnd: end
|
|
130
|
+
};
|
|
131
|
+
}
|
|
100
132
|
function validStoreDirectory(value) {
|
|
101
133
|
if (typeof value !== 'string' || value.trim() !== value || value.length === 0)
|
|
102
134
|
return false;
|
package/dist/connector.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { dirname, resolve } from 'node:path';
|
|
2
2
|
import { createEnvelope } from '@myagentroam/protocol';
|
|
3
|
-
import { hostedConversationStorePath, loadNodeConfig, nodeDataDirectory, nodeDatabasePath } from './config.js';
|
|
3
|
+
import { hostedConversationStorePath, loadNodeConfig, directTransferUdpPortRange, nodeDataDirectory, nodeDatabasePath } from './config.js';
|
|
4
4
|
import { NodeMetrics, nodeLog } from './operational.js';
|
|
5
5
|
import { assertLocalSqlitePath } from './storage.js';
|
|
6
6
|
import { detectCapabilitiesAsync, unavailableCapabilities } from './capabilities.js';
|
|
@@ -32,6 +32,7 @@ import { WorkspaceService } from './service/workspace-service.js';
|
|
|
32
32
|
import { WorkspaceWatchService } from './service/workspace-watch-service.js';
|
|
33
33
|
import { WorkspaceFileService } from './service/workspace-file-service.js';
|
|
34
34
|
import { WorkspaceUploadService } from './service/workspace-upload-service.js';
|
|
35
|
+
import { DirectTransferService } from './service/direct-transfer-service.js';
|
|
35
36
|
import { WorkspaceChangeService } from './service/workspace-change-service.js';
|
|
36
37
|
import { WorkspaceSessionService } from './service/workspace-session-service.js';
|
|
37
38
|
import { WorkbenchEventService } from './service/workbench-event-service.js';
|
|
@@ -271,6 +272,7 @@ export class NodeConnector {
|
|
|
271
272
|
this.workspaceFileService.dispose(workspaceId);
|
|
272
273
|
this.workspaceCoordinator.invalidate(workspaceId);
|
|
273
274
|
});
|
|
275
|
+
directTransferService;
|
|
274
276
|
workspaceChangeService = new WorkspaceChangeService(this.workspaceState);
|
|
275
277
|
workspaceWorkbenchService;
|
|
276
278
|
database;
|
|
@@ -668,11 +670,13 @@ export class NodeConnector {
|
|
|
668
670
|
return;
|
|
669
671
|
const updated = this.missionRuntime.recordTurn(input.sessionId);
|
|
670
672
|
const session = this.runtime.getAgentSession(input.sessionId);
|
|
671
|
-
|
|
672
|
-
this.publishMissionState(session, updated);
|
|
673
|
+
const run = this.runtime.getRun(input.runId);
|
|
673
674
|
await this.enqueueMissionContinuation({
|
|
674
675
|
sessionId: input.sessionId,
|
|
675
676
|
mission: { id: mission.id, revision: mission.revision },
|
|
677
|
+
...(run?.initiatedByUserId === undefined
|
|
678
|
+
? {}
|
|
679
|
+
: { initiatedByUserId: run.initiatedByUserId }),
|
|
676
680
|
secretEnvironment: input.secretEnvironment,
|
|
677
681
|
...(input.personalInstructions === undefined
|
|
678
682
|
? {}
|
|
@@ -683,6 +687,17 @@ export class NodeConnector {
|
|
|
683
687
|
? {}
|
|
684
688
|
: { marAgentRuntimeScopeId: input.marAgentRuntimeScopeId })
|
|
685
689
|
});
|
|
690
|
+
if (updated !== undefined && session !== undefined)
|
|
691
|
+
this.publishMissionState(session, updated);
|
|
692
|
+
},
|
|
693
|
+
afterExecutionStartFailed: (input, error) => {
|
|
694
|
+
nodeLog('mission.execution-start.failed', {
|
|
695
|
+
sessionId: input.sessionId,
|
|
696
|
+
runId: input.runId,
|
|
697
|
+
missionId: input.mission?.id,
|
|
698
|
+
revision: input.mission?.revision,
|
|
699
|
+
code: safeErrorCode(error, 'MISSION_EXECUTION_START_FAILED')
|
|
700
|
+
});
|
|
686
701
|
},
|
|
687
702
|
missionMcp: async (input) => {
|
|
688
703
|
if (input.mission === undefined)
|
|
@@ -1049,6 +1064,7 @@ export class NodeConnector {
|
|
|
1049
1064
|
onReconnectScheduled: () => this.metrics.reconnectScheduled(),
|
|
1050
1065
|
onMessage: (raw) => this.handleMessage(raw),
|
|
1051
1066
|
onClose: (code, reason) => {
|
|
1067
|
+
this.directTransferService?.close();
|
|
1052
1068
|
this.runtimeCredentials.disconnected();
|
|
1053
1069
|
this.terminalChannel.disconnect();
|
|
1054
1070
|
if (code === 4000 && reason === 'NODE_CONNECTION_REPLACED')
|
|
@@ -1059,6 +1075,36 @@ export class NodeConnector {
|
|
|
1059
1075
|
void this.terminalManager.shutdown();
|
|
1060
1076
|
}
|
|
1061
1077
|
});
|
|
1078
|
+
this.directTransferService = new DirectTransferService({
|
|
1079
|
+
nodeGeneration: () => this.nodeGeneration,
|
|
1080
|
+
send: (type, payload) => this.send(type, payload),
|
|
1081
|
+
udpPortRange: () => directTransferUdpPortRange(this.config),
|
|
1082
|
+
openUpload: async (descriptor) => {
|
|
1083
|
+
if (descriptor.resource.kind !== 'WORKSPACE_FILE')
|
|
1084
|
+
throw new Error('DIRECT_RESOURCE_UNSUPPORTED');
|
|
1085
|
+
const workspace = this.workspaceCoordinator.require(descriptor.resource.workspaceId);
|
|
1086
|
+
const upload = await this.workspaceUploadService.createDirect(workspace, {
|
|
1087
|
+
path: descriptor.resource.path,
|
|
1088
|
+
size: descriptor.size,
|
|
1089
|
+
sha256: descriptor.sha256,
|
|
1090
|
+
overwrite: descriptor.overwrite === true,
|
|
1091
|
+
...(descriptor.composerAttachment === undefined
|
|
1092
|
+
? {}
|
|
1093
|
+
: { composerAttachment: descriptor.composerAttachment })
|
|
1094
|
+
});
|
|
1095
|
+
return {
|
|
1096
|
+
offset: upload.receivedBytes,
|
|
1097
|
+
write: (bytes) => this.workspaceUploadService.writeDirect(upload.uploadId, bytes),
|
|
1098
|
+
complete: (size, sha256) => this.workspaceUploadService.completeDirect(upload.uploadId, size, sha256),
|
|
1099
|
+
cancel: () => this.workspaceUploadService.cancel(upload.uploadId)
|
|
1100
|
+
};
|
|
1101
|
+
},
|
|
1102
|
+
openDownload: async (descriptor) => {
|
|
1103
|
+
if (this.workspaceWorkbenchService === undefined)
|
|
1104
|
+
throw new Error('DIRECT_RESOURCE_UNAVAILABLE');
|
|
1105
|
+
return this.workspaceWorkbenchService.directDownload(descriptor);
|
|
1106
|
+
}
|
|
1107
|
+
});
|
|
1062
1108
|
this.controlMessages = new NodeControlMessageService({
|
|
1063
1109
|
config: () => this.config,
|
|
1064
1110
|
register: (nodeId, credential) => this.connectionLifecycle.register(nodeId, credential),
|
|
@@ -1067,6 +1113,12 @@ export class NodeConnector {
|
|
|
1067
1113
|
inspectWorkspace: (envelope) => this.workspaceCoordinator.inspect(envelope),
|
|
1068
1114
|
handleNodeRequest: (envelope) => this.nodeRequests.handle(envelope),
|
|
1069
1115
|
handleRuntimeCredentialResponse: (envelope) => this.runtimeCredentials.handle(envelope),
|
|
1116
|
+
handleDirectTransfer: (envelope) => this.directTransferService.handle({
|
|
1117
|
+
type: envelope.type,
|
|
1118
|
+
...(typeof envelope.payload === 'object' && envelope.payload !== null
|
|
1119
|
+
? envelope.payload
|
|
1120
|
+
: {})
|
|
1121
|
+
}),
|
|
1070
1122
|
reconcileRuns: (envelope) => this.runEventBridge.reconcile(envelope),
|
|
1071
1123
|
runners: this.runners,
|
|
1072
1124
|
deliverSessionChannelMessage: (envelope) => this.runnerChannelMessages.handle(envelope),
|
|
@@ -1134,6 +1186,7 @@ export class NodeConnector {
|
|
|
1134
1186
|
this.runnerService = new RunnerService(requireDatabase, () => this.capabilities, this.runners, this.runnerUsageReader);
|
|
1135
1187
|
this.workbenchManifestService = new WorkbenchManifestService({
|
|
1136
1188
|
capabilities: () => this.capabilities,
|
|
1189
|
+
nodeGeneration: () => this.nodeGeneration,
|
|
1137
1190
|
runners: this.runnerService,
|
|
1138
1191
|
terminals: this.terminalManager
|
|
1139
1192
|
});
|
|
@@ -1386,6 +1439,9 @@ export class NodeConnector {
|
|
|
1386
1439
|
content: this.missionRuntime.prompt(input.sessionId, 'continue'),
|
|
1387
1440
|
deliveryIntent: 'QUEUE',
|
|
1388
1441
|
mission: input.mission,
|
|
1442
|
+
...(input.initiatedByUserId === undefined
|
|
1443
|
+
? {}
|
|
1444
|
+
: { initiatedByUserId: input.initiatedByUserId }),
|
|
1389
1445
|
secretEnvironment: input.secretEnvironment,
|
|
1390
1446
|
...(input.personalInstructions === undefined
|
|
1391
1447
|
? {}
|
|
@@ -1407,8 +1463,16 @@ export class NodeConnector {
|
|
|
1407
1463
|
}
|
|
1408
1464
|
this.clearMissionRetry(input.sessionId);
|
|
1409
1465
|
}
|
|
1410
|
-
catch {
|
|
1466
|
+
catch (error) {
|
|
1411
1467
|
const current = this.missionRuntime.get(input.sessionId);
|
|
1468
|
+
const failedAttempts = (input.failedAttempts ?? 0) + 1;
|
|
1469
|
+
nodeLog('mission.continuation.enqueue.failed', {
|
|
1470
|
+
sessionId: input.sessionId,
|
|
1471
|
+
missionId: input.mission.id,
|
|
1472
|
+
revision: input.mission.revision,
|
|
1473
|
+
attempt: failedAttempts,
|
|
1474
|
+
code: safeErrorCode(error, 'MISSION_CONTINUATION_ENQUEUE_FAILED')
|
|
1475
|
+
});
|
|
1412
1476
|
if (current?.status !== 'active' ||
|
|
1413
1477
|
current.id !== input.mission.id ||
|
|
1414
1478
|
current.revision !== input.mission.revision) {
|
|
@@ -1416,7 +1480,6 @@ export class NodeConnector {
|
|
|
1416
1480
|
this.missionContinuationPending.delete(input.sessionId);
|
|
1417
1481
|
return;
|
|
1418
1482
|
}
|
|
1419
|
-
const failedAttempts = (input.failedAttempts ?? 0) + 1;
|
|
1420
1483
|
const session = this.runtime.getAgentSession(input.sessionId);
|
|
1421
1484
|
if (failedAttempts > 3 || session === undefined) {
|
|
1422
1485
|
this.clearMissionRetry(input.sessionId);
|
|
@@ -1537,6 +1600,7 @@ export class NodeConnector {
|
|
|
1537
1600
|
const terminalDrain = this.terminalManager.shutdown();
|
|
1538
1601
|
this.terminalChannel.stop();
|
|
1539
1602
|
this.runtimeCredentials.close();
|
|
1603
|
+
this.directTransferService.close();
|
|
1540
1604
|
this.controlChannel.stop();
|
|
1541
1605
|
const database = this.database;
|
|
1542
1606
|
this.database = undefined;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { MarAgentConversationNormalizer } from './conversation-normalizer.js';
|
|
2
|
+
import { publicMissionHistoryText } from '../../service/mission-runtime.js';
|
|
2
3
|
export function projectMarAgentHistory(session, entries, options = {}) {
|
|
3
4
|
const turns = new Map();
|
|
4
5
|
const normalizer = new MarAgentConversationNormalizer();
|
|
@@ -42,7 +43,7 @@ export function projectMarAgentHistory(session, entries, options = {}) {
|
|
|
42
43
|
const registeredImages = images === undefined ? [] : (options.registerInputImages?.(entry) ?? []);
|
|
43
44
|
replace(turn, historyItem(session, turn, id, 'user_message', 'COMPLETED', {
|
|
44
45
|
clientMessageId: entry.clientMessageId ?? null,
|
|
45
|
-
text: entry.text,
|
|
46
|
+
text: publicMissionHistoryText(entry.text),
|
|
46
47
|
contexts: [],
|
|
47
48
|
delivery: 'ACCEPTED',
|
|
48
49
|
...(images?.length
|
|
@@ -321,7 +321,7 @@ export class ContinuousRunCoordinator {
|
|
|
321
321
|
this.startedExecutions.add(executionId);
|
|
322
322
|
void this.options
|
|
323
323
|
.afterExecutionStarted?.(pending)
|
|
324
|
-
.catch(() =>
|
|
324
|
+
.catch((error) => this.options.afterExecutionStartFailed?.(pending, error));
|
|
325
325
|
}
|
|
326
326
|
hasExecutionStarted(executionId) {
|
|
327
327
|
return this.startedExecutions.has(executionId);
|
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
import { directDataControlFrameSchema, directNodeCommandSchema } from '@myagentroam/protocol';
|
|
2
|
+
import { RTCPeerConnection } from 'node-datachannel/polyfill';
|
|
3
|
+
const MAX_HOST_CANDIDATES = 32;
|
|
4
|
+
const MAX_ACTIVE_SESSIONS = 32;
|
|
5
|
+
const DATA_CHANNEL_HIGH_WATER_BYTES = 4 * 1024 * 1024;
|
|
6
|
+
const DATA_CHANNEL_LOW_WATER_BYTES = 1024 * 1024;
|
|
7
|
+
const LEASE_GRACE_MS = 30_000;
|
|
8
|
+
const LEASE_CHECK_MS = 5_000;
|
|
9
|
+
export class DirectTransferService {
|
|
10
|
+
options;
|
|
11
|
+
sessions = new Map();
|
|
12
|
+
now;
|
|
13
|
+
createPeerConnection;
|
|
14
|
+
leaseGraceMs;
|
|
15
|
+
leaseCheckMs;
|
|
16
|
+
constructor(options) {
|
|
17
|
+
this.options = options;
|
|
18
|
+
this.now = options.now ?? Date.now;
|
|
19
|
+
this.createPeerConnection =
|
|
20
|
+
options.createPeerConnection ??
|
|
21
|
+
(() => {
|
|
22
|
+
const range = options.udpPortRange?.() ?? { begin: 49_152, end: 49_215 };
|
|
23
|
+
return new RTCPeerConnection({
|
|
24
|
+
iceServers: [],
|
|
25
|
+
portRangeBegin: range.begin,
|
|
26
|
+
portRangeEnd: range.end
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
this.leaseGraceMs = options.leaseGraceMs ?? LEASE_GRACE_MS;
|
|
30
|
+
this.leaseCheckMs = options.leaseCheckMs ?? LEASE_CHECK_MS;
|
|
31
|
+
}
|
|
32
|
+
handle(value) {
|
|
33
|
+
const parsed = directNodeCommandSchema.safeParse(value);
|
|
34
|
+
if (!parsed.success)
|
|
35
|
+
return false;
|
|
36
|
+
void this.dispatch(parsed.data).catch((error) => {
|
|
37
|
+
const directSessionId = typeof value === 'object' &&
|
|
38
|
+
value !== null &&
|
|
39
|
+
'directSessionId' in value &&
|
|
40
|
+
typeof value.directSessionId === 'string'
|
|
41
|
+
? value.directSessionId
|
|
42
|
+
: undefined;
|
|
43
|
+
if (directSessionId !== undefined)
|
|
44
|
+
this.fail(directSessionId, errorCode(error), errorMessage(error));
|
|
45
|
+
});
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
close() {
|
|
49
|
+
for (const session of [...this.sessions.values()])
|
|
50
|
+
void this.remove(session, 'DIRECT_NODE_STOPPED');
|
|
51
|
+
}
|
|
52
|
+
async dispatch(command) {
|
|
53
|
+
if (command.type === 'direct.prepare') {
|
|
54
|
+
this.prepare(command);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const session = this.sessions.get(command.directSessionId);
|
|
58
|
+
if (session === undefined)
|
|
59
|
+
throw new Error('DIRECT_SESSION_INVALID');
|
|
60
|
+
if (command.type === 'direct.signal') {
|
|
61
|
+
await this.signal(session, command.signal);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (command.type === 'direct.grant') {
|
|
65
|
+
if (command.browserFingerprint !== fingerprint(session.peer.remoteDescription?.sdp) ||
|
|
66
|
+
command.nodeFingerprint !== fingerprint(session.peer.localDescription?.sdp) ||
|
|
67
|
+
command.openExpiresAt <= this.now() ||
|
|
68
|
+
command.expiresAt > session.command.expiresAt)
|
|
69
|
+
throw new Error('DIRECT_GRANT_INVALID');
|
|
70
|
+
session.grant = command;
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (command.type === 'direct.heartbeat') {
|
|
74
|
+
if (session.command.expiresAt <= this.now())
|
|
75
|
+
await this.remove(session, 'DIRECT_SESSION_EXPIRED');
|
|
76
|
+
else
|
|
77
|
+
session.lastLeaseAliveAt = this.now();
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
await this.remove(session, command.code);
|
|
81
|
+
}
|
|
82
|
+
prepare(command) {
|
|
83
|
+
if (command.nodeGeneration !== this.options.nodeGeneration())
|
|
84
|
+
throw new Error('DIRECT_NODE_GENERATION_MISMATCH');
|
|
85
|
+
if (this.sessions.has(command.directSessionId))
|
|
86
|
+
throw new Error('DIRECT_SESSION_CONFLICT');
|
|
87
|
+
if (this.sessions.size >= MAX_ACTIVE_SESSIONS)
|
|
88
|
+
throw new Error('DIRECT_SESSION_LIMIT_REACHED');
|
|
89
|
+
const peer = this.createPeerConnection();
|
|
90
|
+
const session = {
|
|
91
|
+
command,
|
|
92
|
+
peer,
|
|
93
|
+
opened: false,
|
|
94
|
+
uploadCommitted: false,
|
|
95
|
+
processing: Promise.resolve(),
|
|
96
|
+
sentCandidateKeys: new Set()
|
|
97
|
+
};
|
|
98
|
+
this.sessions.set(command.directSessionId, session);
|
|
99
|
+
peer.onicecandidate = (event) => {
|
|
100
|
+
const candidate = event.candidate;
|
|
101
|
+
if (candidate === null) {
|
|
102
|
+
this.emit(command.directSessionId, {
|
|
103
|
+
type: 'direct.signal',
|
|
104
|
+
directSessionId: command.directSessionId,
|
|
105
|
+
signal: { kind: 'END_OF_CANDIDATES' }
|
|
106
|
+
});
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const json = candidate.toJSON();
|
|
110
|
+
if (typeof json.candidate !== 'string' ||
|
|
111
|
+
!acceptHostCandidate(json.candidate, session.sentCandidateKeys))
|
|
112
|
+
return;
|
|
113
|
+
this.emit(command.directSessionId, {
|
|
114
|
+
type: 'direct.signal',
|
|
115
|
+
directSessionId: command.directSessionId,
|
|
116
|
+
signal: {
|
|
117
|
+
kind: 'CANDIDATE',
|
|
118
|
+
candidate: {
|
|
119
|
+
candidate: json.candidate,
|
|
120
|
+
sdpMid: json.sdpMid ?? null,
|
|
121
|
+
sdpMLineIndex: json.sdpMLineIndex ?? null
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
};
|
|
126
|
+
peer.ondatachannel = (event) => this.attachChannel(session, event.channel);
|
|
127
|
+
peer.onconnectionstatechange = () => {
|
|
128
|
+
if (peer.connectionState === 'connected')
|
|
129
|
+
this.emit(session.command.directSessionId, {
|
|
130
|
+
type: 'direct.connected',
|
|
131
|
+
directSessionId: session.command.directSessionId
|
|
132
|
+
});
|
|
133
|
+
if (peer.connectionState === 'failed' || peer.connectionState === 'closed')
|
|
134
|
+
void this.remove(session, 'DIRECT_CONNECTION_FAILED');
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
async signal(session, signal) {
|
|
138
|
+
if (signal.kind === 'CANDIDATE') {
|
|
139
|
+
if (!safeHostCandidate(signal.candidate.candidate))
|
|
140
|
+
throw new Error('DIRECT_CANDIDATE_REJECTED');
|
|
141
|
+
await session.peer.addIceCandidate(signal.candidate);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (signal.kind === 'END_OF_CANDIDATES') {
|
|
145
|
+
await session.peer.addIceCandidate(null);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (!hostOnlySdp(signal.sdp))
|
|
149
|
+
throw new Error('DIRECT_CANDIDATE_REJECTED');
|
|
150
|
+
await session.peer.setRemoteDescription({
|
|
151
|
+
type: signal.kind === 'OFFER' ? 'offer' : 'answer',
|
|
152
|
+
sdp: signal.sdp
|
|
153
|
+
});
|
|
154
|
+
if (signal.kind !== 'OFFER')
|
|
155
|
+
return;
|
|
156
|
+
const answer = await session.peer.createAnswer();
|
|
157
|
+
await session.peer.setLocalDescription(answer);
|
|
158
|
+
const localSdp = session.peer.localDescription?.sdp;
|
|
159
|
+
const filtered = filterIceCandidates(localSdp, session.sentCandidateKeys);
|
|
160
|
+
const sdp = filtered.sdp;
|
|
161
|
+
const localFingerprint = fingerprint(localSdp);
|
|
162
|
+
if (sdp === undefined || localFingerprint === undefined)
|
|
163
|
+
throw new Error('DIRECT_FINGERPRINT_MISSING');
|
|
164
|
+
this.emit(session.command.directSessionId, {
|
|
165
|
+
type: 'direct.signal',
|
|
166
|
+
directSessionId: session.command.directSessionId,
|
|
167
|
+
signal: { kind: 'ANSWER', sdp, fingerprint: localFingerprint }
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
attachChannel(session, channel) {
|
|
171
|
+
session.channel = channel;
|
|
172
|
+
channel.binaryType = 'arraybuffer';
|
|
173
|
+
channel.onmessage = (event) => {
|
|
174
|
+
session.processing = session.processing
|
|
175
|
+
.then(() => this.channelMessage(session, event.data))
|
|
176
|
+
.catch((error) => this.fail(session.command.directSessionId, errorCode(error), errorMessage(error)));
|
|
177
|
+
};
|
|
178
|
+
channel.onclose = () => {
|
|
179
|
+
if (this.sessions.get(session.command.directSessionId) === session)
|
|
180
|
+
void this.remove(session, 'DIRECT_CHANNEL_CLOSED');
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
async channelMessage(session, data) {
|
|
184
|
+
if (!session.opened) {
|
|
185
|
+
if (typeof data !== 'string')
|
|
186
|
+
throw new Error('DIRECT_OPEN_REQUIRED');
|
|
187
|
+
const parsed = directDataControlFrameSchema.parse(JSON.parse(data));
|
|
188
|
+
if (parsed.type !== 'OPEN')
|
|
189
|
+
throw new Error('DIRECT_OPEN_REQUIRED');
|
|
190
|
+
const grant = session.grant;
|
|
191
|
+
if (grant === undefined ||
|
|
192
|
+
parsed.directSessionId !== session.command.directSessionId ||
|
|
193
|
+
parsed.grant !== grant.grant ||
|
|
194
|
+
grant.openExpiresAt <= this.now() ||
|
|
195
|
+
grant.expiresAt <= this.now())
|
|
196
|
+
throw new Error('DIRECT_GRANT_INVALID');
|
|
197
|
+
session.opened = true;
|
|
198
|
+
if (session.command.purpose === 'TRANSFER')
|
|
199
|
+
this.startLeaseWatch(session);
|
|
200
|
+
await this.open(session);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
if (session.command.purpose !== 'TRANSFER' || session.command.transfer === undefined)
|
|
204
|
+
throw new Error('DIRECT_FRAME_INVALID');
|
|
205
|
+
if (session.command.transfer.direction === 'DOWNLOAD') {
|
|
206
|
+
if (typeof data !== 'string')
|
|
207
|
+
throw new Error('DIRECT_FRAME_INVALID');
|
|
208
|
+
const control = directDataControlFrameSchema.parse(JSON.parse(data));
|
|
209
|
+
if (control.type !== 'ACK' || control.offset !== session.command.transfer.size)
|
|
210
|
+
throw new Error('DIRECT_FRAME_INVALID');
|
|
211
|
+
this.emit(session.command.directSessionId, {
|
|
212
|
+
type: 'direct.complete',
|
|
213
|
+
directSessionId: session.command.directSessionId,
|
|
214
|
+
size: session.command.transfer.size,
|
|
215
|
+
sha256: session.command.transfer.sha256
|
|
216
|
+
});
|
|
217
|
+
await this.remove(session);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
if (typeof data === 'string') {
|
|
221
|
+
const control = directDataControlFrameSchema.parse(JSON.parse(data));
|
|
222
|
+
if (control.type !== 'COMPLETE' || session.upload === undefined)
|
|
223
|
+
throw new Error('DIRECT_FRAME_INVALID');
|
|
224
|
+
if (control.size !== session.command.transfer.size ||
|
|
225
|
+
control.sha256 !== session.command.transfer.sha256)
|
|
226
|
+
throw new Error('DIRECT_INTEGRITY_FAILED');
|
|
227
|
+
await session.upload.complete(control.size, control.sha256);
|
|
228
|
+
session.uploadCommitted = true;
|
|
229
|
+
this.emit(session.command.directSessionId, {
|
|
230
|
+
type: 'direct.complete',
|
|
231
|
+
directSessionId: session.command.directSessionId,
|
|
232
|
+
size: control.size,
|
|
233
|
+
sha256: control.sha256
|
|
234
|
+
});
|
|
235
|
+
await this.remove(session);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
const bytes = toBytes(data);
|
|
239
|
+
if (bytes.byteLength === 0 || bytes.byteLength > 256 * 1024 || session.upload === undefined)
|
|
240
|
+
throw new Error('DIRECT_CHUNK_INVALID');
|
|
241
|
+
const offset = await session.upload.write(bytes);
|
|
242
|
+
session.channel?.send(JSON.stringify({ type: 'ACK', offset }));
|
|
243
|
+
}
|
|
244
|
+
async open(session) {
|
|
245
|
+
if (session.command.purpose === 'PROBE') {
|
|
246
|
+
session.channel?.send(JSON.stringify({ type: 'ACK', offset: 0 }));
|
|
247
|
+
this.emit(session.command.directSessionId, {
|
|
248
|
+
type: 'direct.probe-result',
|
|
249
|
+
directSessionId: session.command.directSessionId,
|
|
250
|
+
result: 'DIRECT_AVAILABLE'
|
|
251
|
+
});
|
|
252
|
+
await this.remove(session);
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
const descriptor = session.command.transfer;
|
|
256
|
+
if (descriptor === undefined)
|
|
257
|
+
throw new Error('DIRECT_TRANSFER_INVALID');
|
|
258
|
+
if (descriptor.direction === 'UPLOAD') {
|
|
259
|
+
if (this.options.openUpload === undefined)
|
|
260
|
+
throw new Error('DIRECT_UPLOAD_UNAVAILABLE');
|
|
261
|
+
session.upload = await this.options.openUpload(descriptor);
|
|
262
|
+
session.channel?.send(JSON.stringify({ type: 'ACK', offset: session.upload.offset }));
|
|
263
|
+
this.emit(session.command.directSessionId, {
|
|
264
|
+
type: 'direct.opened',
|
|
265
|
+
directSessionId: session.command.directSessionId,
|
|
266
|
+
offset: session.upload.offset
|
|
267
|
+
});
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
if (this.options.openDownload === undefined)
|
|
271
|
+
throw new Error('DIRECT_DOWNLOAD_UNAVAILABLE');
|
|
272
|
+
const source = await this.options.openDownload(descriptor);
|
|
273
|
+
session.channel?.send(JSON.stringify({ type: 'ACK', offset: source.offset }));
|
|
274
|
+
this.emit(session.command.directSessionId, {
|
|
275
|
+
type: 'direct.opened',
|
|
276
|
+
directSessionId: session.command.directSessionId,
|
|
277
|
+
offset: source.offset
|
|
278
|
+
});
|
|
279
|
+
for await (const chunk of source.chunks) {
|
|
280
|
+
for (let offset = 0; offset < chunk.byteLength; offset += 256 * 1024) {
|
|
281
|
+
if (this.sessions.get(session.command.directSessionId) !== session)
|
|
282
|
+
return;
|
|
283
|
+
const channel = session.channel;
|
|
284
|
+
if (channel === undefined)
|
|
285
|
+
throw new Error('DIRECT_CHANNEL_CLOSED');
|
|
286
|
+
await this.waitForSendCapacity(session, channel);
|
|
287
|
+
const end = Math.min(chunk.byteLength, offset + 256 * 1024);
|
|
288
|
+
channel.send(chunk.buffer.slice(chunk.byteOffset + offset, chunk.byteOffset + end));
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
session.channel?.send(JSON.stringify({
|
|
292
|
+
type: 'COMPLETE',
|
|
293
|
+
size: descriptor.size,
|
|
294
|
+
sha256: descriptor.sha256
|
|
295
|
+
}));
|
|
296
|
+
}
|
|
297
|
+
async waitForSendCapacity(session, channel) {
|
|
298
|
+
if (channel.readyState !== 'open')
|
|
299
|
+
throw new Error('DIRECT_CHANNEL_CLOSED');
|
|
300
|
+
if (channel.bufferedAmount <= DATA_CHANNEL_HIGH_WATER_BYTES)
|
|
301
|
+
return;
|
|
302
|
+
channel.bufferedAmountLowThreshold = DATA_CHANNEL_LOW_WATER_BYTES;
|
|
303
|
+
await new Promise((resolve, reject) => {
|
|
304
|
+
const complete = (error) => {
|
|
305
|
+
channel.removeEventListener('bufferedamountlow', available);
|
|
306
|
+
channel.removeEventListener('close', closed);
|
|
307
|
+
if (error === undefined)
|
|
308
|
+
resolve();
|
|
309
|
+
else
|
|
310
|
+
reject(error);
|
|
311
|
+
};
|
|
312
|
+
const available = () => complete();
|
|
313
|
+
const closed = () => complete(new Error('DIRECT_CHANNEL_CLOSED'));
|
|
314
|
+
channel.addEventListener('bufferedamountlow', available, { once: true });
|
|
315
|
+
channel.addEventListener('close', closed, { once: true });
|
|
316
|
+
if (this.sessions.get(session.command.directSessionId) !== session ||
|
|
317
|
+
channel.readyState !== 'open')
|
|
318
|
+
closed();
|
|
319
|
+
else if (channel.bufferedAmount <= DATA_CHANNEL_LOW_WATER_BYTES)
|
|
320
|
+
available();
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
startLeaseWatch(session) {
|
|
324
|
+
session.lastLeaseAliveAt = this.now();
|
|
325
|
+
session.leaseTimer = setInterval(() => {
|
|
326
|
+
const now = this.now();
|
|
327
|
+
if (session.command.expiresAt <= now) {
|
|
328
|
+
void this.remove(session, 'DIRECT_SESSION_EXPIRED');
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
if (now - (session.lastLeaseAliveAt ?? now) > this.leaseGraceMs)
|
|
332
|
+
void this.remove(session, 'DIRECT_LEASE_EXPIRED');
|
|
333
|
+
}, this.leaseCheckMs);
|
|
334
|
+
}
|
|
335
|
+
emit(directSessionId, event) {
|
|
336
|
+
if (this.sessions.has(directSessionId))
|
|
337
|
+
this.options.send(event.type, event);
|
|
338
|
+
}
|
|
339
|
+
fail(directSessionId, code, message) {
|
|
340
|
+
const session = this.sessions.get(directSessionId);
|
|
341
|
+
this.options.send('direct.error', { type: 'direct.error', directSessionId, code, message });
|
|
342
|
+
if (session !== undefined)
|
|
343
|
+
void this.remove(session);
|
|
344
|
+
}
|
|
345
|
+
async remove(session, code) {
|
|
346
|
+
if (!this.sessions.delete(session.command.directSessionId))
|
|
347
|
+
return;
|
|
348
|
+
if (session.leaseTimer !== undefined)
|
|
349
|
+
clearInterval(session.leaseTimer);
|
|
350
|
+
if (!session.uploadCommitted)
|
|
351
|
+
await session.upload?.cancel().catch(() => undefined);
|
|
352
|
+
session.channel?.close();
|
|
353
|
+
session.peer.close();
|
|
354
|
+
if (code !== undefined)
|
|
355
|
+
this.options.send('direct.error', {
|
|
356
|
+
type: 'direct.error',
|
|
357
|
+
directSessionId: session.command.directSessionId,
|
|
358
|
+
code,
|
|
359
|
+
message: directErrorMessage(code)
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
export function fingerprint(sdp) {
|
|
364
|
+
const match = sdp?.match(/^a=fingerprint:(sha-256 [A-F0-9:]+)\r?$/imu);
|
|
365
|
+
return match?.[1];
|
|
366
|
+
}
|
|
367
|
+
function safeHostCandidate(candidate) {
|
|
368
|
+
if (!/\styp host(?:\s|$)/u.test(candidate))
|
|
369
|
+
return false;
|
|
370
|
+
const address = candidate.trim().split(/\s+/u)[4];
|
|
371
|
+
if (address === undefined)
|
|
372
|
+
return false;
|
|
373
|
+
return address.length > 0;
|
|
374
|
+
}
|
|
375
|
+
function hostOnlySdp(sdp) {
|
|
376
|
+
return sdp
|
|
377
|
+
.split(/\r?\n/u)
|
|
378
|
+
.filter((line) => line.startsWith('a=candidate:'))
|
|
379
|
+
.every((line) => safeHostCandidate(line.slice(2)));
|
|
380
|
+
}
|
|
381
|
+
function candidateKey(candidate) {
|
|
382
|
+
const fields = candidate.trim().split(/\s+/u);
|
|
383
|
+
const component = fields[1];
|
|
384
|
+
const protocol = fields[2]?.toLowerCase();
|
|
385
|
+
const address = fields[4]?.toLowerCase();
|
|
386
|
+
if (component === undefined || protocol === undefined || address === undefined)
|
|
387
|
+
return undefined;
|
|
388
|
+
return `${component}:${protocol}:${address}`;
|
|
389
|
+
}
|
|
390
|
+
function acceptHostCandidate(candidate, accepted) {
|
|
391
|
+
if (!safeHostCandidate(candidate))
|
|
392
|
+
return false;
|
|
393
|
+
const key = candidateKey(candidate);
|
|
394
|
+
if (key === undefined || accepted.has(key) || accepted.size >= MAX_HOST_CANDIDATES)
|
|
395
|
+
return false;
|
|
396
|
+
accepted.add(key);
|
|
397
|
+
return true;
|
|
398
|
+
}
|
|
399
|
+
function filterIceCandidates(sdp, accepted) {
|
|
400
|
+
if (sdp === undefined)
|
|
401
|
+
return { sdp: undefined };
|
|
402
|
+
const included = new Set();
|
|
403
|
+
const lines = sdp.split(/\r?\n/u).filter((line) => {
|
|
404
|
+
if (!line.startsWith('a=candidate:'))
|
|
405
|
+
return true;
|
|
406
|
+
const candidate = line.slice(2);
|
|
407
|
+
if (!safeHostCandidate(candidate))
|
|
408
|
+
return false;
|
|
409
|
+
const key = candidateKey(candidate);
|
|
410
|
+
if (key === undefined || included.has(key))
|
|
411
|
+
return false;
|
|
412
|
+
if (!accepted.has(key) && accepted.size >= MAX_HOST_CANDIDATES)
|
|
413
|
+
return false;
|
|
414
|
+
accepted.add(key);
|
|
415
|
+
included.add(key);
|
|
416
|
+
return true;
|
|
417
|
+
});
|
|
418
|
+
return { sdp: lines.join('\r\n') };
|
|
419
|
+
}
|
|
420
|
+
function toBytes(value) {
|
|
421
|
+
if (value instanceof ArrayBuffer)
|
|
422
|
+
return new Uint8Array(value);
|
|
423
|
+
if (ArrayBuffer.isView(value))
|
|
424
|
+
return new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
|
|
425
|
+
throw new Error('DIRECT_CHUNK_INVALID');
|
|
426
|
+
}
|
|
427
|
+
function errorCode(error) {
|
|
428
|
+
return error instanceof Error && /^[A-Z][A-Z0-9_]{0,127}$/u.test(error.message)
|
|
429
|
+
? error.message
|
|
430
|
+
: 'DIRECT_INTERNAL_ERROR';
|
|
431
|
+
}
|
|
432
|
+
function errorMessage(error) {
|
|
433
|
+
const code = errorCode(error);
|
|
434
|
+
return code === 'DIRECT_INTERNAL_ERROR'
|
|
435
|
+
? 'The Node could not complete the direct transfer operation.'
|
|
436
|
+
: directErrorMessage(code);
|
|
437
|
+
}
|
|
438
|
+
function directErrorMessage(code) {
|
|
439
|
+
const messages = {
|
|
440
|
+
DIRECT_NODE_GENERATION_MISMATCH: 'The Node connection generation changed.',
|
|
441
|
+
DIRECT_SESSION_INVALID: 'The direct transfer session is unavailable.',
|
|
442
|
+
DIRECT_SESSION_CONFLICT: 'The direct transfer session already exists.',
|
|
443
|
+
DIRECT_SESSION_LIMIT_REACHED: 'The direct transfer session limit has been reached.',
|
|
444
|
+
DIRECT_CANDIDATE_REJECTED: 'The direct transfer candidate is not a host candidate.',
|
|
445
|
+
DIRECT_GRANT_INVALID: 'The direct transfer grant is invalid or expired.',
|
|
446
|
+
DIRECT_CONNECTION_FAILED: 'The direct connection failed.',
|
|
447
|
+
DIRECT_CHANNEL_CLOSED: 'The direct data channel closed.',
|
|
448
|
+
DIRECT_SESSION_EXPIRED: 'The direct transfer session expired.',
|
|
449
|
+
DIRECT_LEASE_EXPIRED: 'The Server authorization lease expired.',
|
|
450
|
+
DIRECT_NODE_STOPPED: 'The Node stopped the direct transfer service.'
|
|
451
|
+
};
|
|
452
|
+
return messages[code] ?? 'The direct transfer operation failed.';
|
|
453
|
+
}
|
|
@@ -14,6 +14,16 @@ export function stripMissionStartPrompt(text) {
|
|
|
14
14
|
const close = text.indexOf(START_CLOSE);
|
|
15
15
|
return close === -1 ? '' : text.slice(close + START_CLOSE.length).trimStart();
|
|
16
16
|
}
|
|
17
|
+
export function publicMissionHistoryText(text) {
|
|
18
|
+
if (!text.startsWith(START_OPEN))
|
|
19
|
+
return text;
|
|
20
|
+
const stripped = stripMissionStartPrompt(text);
|
|
21
|
+
if (stripped.length > 0)
|
|
22
|
+
return stripped;
|
|
23
|
+
const close = text.indexOf(START_CLOSE);
|
|
24
|
+
const prompt = close === -1 ? text : text.slice(0, close);
|
|
25
|
+
return prompt.includes('Continue the active Mission.') ? 'Mission Continue' : 'Mission Start';
|
|
26
|
+
}
|
|
17
27
|
export class MissionRuntime {
|
|
18
28
|
changed;
|
|
19
29
|
missions = new Map();
|
|
@@ -37,6 +37,7 @@ export class NodeConnectionLifecycleService {
|
|
|
37
37
|
...(config.conversationHosting === undefined
|
|
38
38
|
? {}
|
|
39
39
|
: { conversationHosting: config.conversationHosting }),
|
|
40
|
+
...(config.directTransfer === undefined ? {} : { directTransfer: config.directTransfer }),
|
|
40
41
|
nodeId,
|
|
41
42
|
credential
|
|
42
43
|
};
|
|
@@ -35,6 +35,9 @@ export class NodeControlMessageService {
|
|
|
35
35
|
}
|
|
36
36
|
if (this.options.handleRuntimeCredentialResponse(envelope))
|
|
37
37
|
return;
|
|
38
|
+
if (envelope.type.startsWith('direct.') &&
|
|
39
|
+
this.options.handleDirectTransfer?.(envelope) === true)
|
|
40
|
+
return;
|
|
38
41
|
if (envelope.type === 'ack')
|
|
39
42
|
return;
|
|
40
43
|
if (envelope.type === 'runs.reconcile') {
|
|
@@ -25,6 +25,9 @@ export class SessionMessageService {
|
|
|
25
25
|
content: input.kind === 'start' ? 'Mission Start' : 'Mission Continue',
|
|
26
26
|
__runnerContent: input.content,
|
|
27
27
|
__missionRef: input.mission,
|
|
28
|
+
...(input.initiatedByUserId === undefined
|
|
29
|
+
? {}
|
|
30
|
+
: { __requestUserId: input.initiatedByUserId }),
|
|
28
31
|
deliveryIntent: input.deliveryIntent,
|
|
29
32
|
secretEnvironment: input.secretEnvironment,
|
|
30
33
|
personalInstructions: input.personalInstructions,
|
|
@@ -23,6 +23,7 @@ export class WorkbenchManifestService {
|
|
|
23
23
|
manifestVersion: 1,
|
|
24
24
|
revision: revisionFor(capabilities),
|
|
25
25
|
observedAt: Date.now(),
|
|
26
|
+
nodeGeneration: this.options.nodeGeneration(),
|
|
26
27
|
platform: capabilities.platform,
|
|
27
28
|
architecture: capabilities.architecture,
|
|
28
29
|
conversation: {
|
|
@@ -45,6 +46,7 @@ export class WorkbenchManifestService {
|
|
|
45
46
|
maxReadBytes: 512 * 1024,
|
|
46
47
|
maxSearchResults: 200
|
|
47
48
|
},
|
|
49
|
+
directTransfer: capabilities.workspaceDirectTransfer,
|
|
48
50
|
git: {
|
|
49
51
|
available: true,
|
|
50
52
|
reasonCode: null,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { tmpdir } from 'node:os';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
2
3
|
import { isAbsolute, win32 } from 'node:path';
|
|
3
4
|
import { listWorkspaceFiles, mutateWorkspaceFile, normalizeFilePreviewPath, readAllowedFileContentRange, readWorkspaceFileContentRange, readAllowedTextFile, readWorkspaceTextFile, writeWorkspaceTextFile, WorkspaceFileIndex } from '../workspace.js';
|
|
4
5
|
const INDEX_CACHE_LIMIT = 8;
|
|
@@ -58,6 +59,54 @@ export class WorkspaceFileService {
|
|
|
58
59
|
? readAllowedFileContentRange(path, this.previewRoots(), offset, limit)
|
|
59
60
|
: readWorkspaceFileContentRange(workspacePath, path, offset, limit);
|
|
60
61
|
}
|
|
62
|
+
readDirect(workspace, input, workspacePath = workspace.path) {
|
|
63
|
+
const service = this;
|
|
64
|
+
return (async function* () {
|
|
65
|
+
const hash = createHash('sha256');
|
|
66
|
+
let offset = 0;
|
|
67
|
+
while (true) {
|
|
68
|
+
const range = (await service.readContent(workspace, {
|
|
69
|
+
path: input.path,
|
|
70
|
+
offset,
|
|
71
|
+
limit: 512 * 1024
|
|
72
|
+
}, workspacePath));
|
|
73
|
+
if (range.size !== input.size)
|
|
74
|
+
throw new Error('DIRECT_FILE_REVISION_CHANGED');
|
|
75
|
+
const bytes = Buffer.from(range.contentBase64, 'base64');
|
|
76
|
+
hash.update(bytes);
|
|
77
|
+
offset += bytes.length;
|
|
78
|
+
if (bytes.length > 0)
|
|
79
|
+
yield bytes;
|
|
80
|
+
if (range.nextOffset === null)
|
|
81
|
+
break;
|
|
82
|
+
if (range.nextOffset !== offset)
|
|
83
|
+
throw new Error('DIRECT_FILE_REVISION_CHANGED');
|
|
84
|
+
}
|
|
85
|
+
if (offset !== input.size || hash.digest('hex') !== input.sha256)
|
|
86
|
+
throw new Error('DIRECT_FILE_REVISION_CHANGED');
|
|
87
|
+
})();
|
|
88
|
+
}
|
|
89
|
+
async directMetadata(workspace, input, workspacePath = workspace.path) {
|
|
90
|
+
const hash = createHash('sha256');
|
|
91
|
+
let offset = 0;
|
|
92
|
+
let size;
|
|
93
|
+
while (true) {
|
|
94
|
+
const range = (await this.readContent(workspace, { ...input, offset, limit: 512 * 1024 }, workspacePath));
|
|
95
|
+
size ??= range.size;
|
|
96
|
+
if (range.size !== size)
|
|
97
|
+
throw new Error('DIRECT_FILE_REVISION_CHANGED');
|
|
98
|
+
const bytes = Buffer.from(range.contentBase64, 'base64');
|
|
99
|
+
hash.update(bytes);
|
|
100
|
+
offset += bytes.length;
|
|
101
|
+
if (range.nextOffset === null)
|
|
102
|
+
break;
|
|
103
|
+
if (range.nextOffset !== offset)
|
|
104
|
+
throw new Error('DIRECT_FILE_REVISION_CHANGED');
|
|
105
|
+
}
|
|
106
|
+
if (size === undefined || offset !== size)
|
|
107
|
+
throw new Error('DIRECT_FILE_REVISION_CHANGED');
|
|
108
|
+
return { size, sha256: hash.digest('hex') };
|
|
109
|
+
}
|
|
61
110
|
async write(workspace, input) {
|
|
62
111
|
if (typeof input.path !== 'string' ||
|
|
63
112
|
typeof input.content !== 'string' ||
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { randomUUID } from 'node:crypto';
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { createReadStream } from 'node:fs';
|
|
2
3
|
import { lstat, mkdir, open, readFile, rename, rm } from 'node:fs/promises';
|
|
3
|
-
import { dirname, join, resolve } from 'node:path';
|
|
4
|
+
import { dirname, join, posix, resolve } from 'node:path';
|
|
4
5
|
import { parseComposerAttachmentUpload, validComposerAttachmentName } from '../util/node-operation-parsers.js';
|
|
5
6
|
import { preflightWorkspaceUpload, workspaceUploadTemporaryName } from '../workspace.js';
|
|
6
7
|
const CHUNK_BYTES = 512 * 1024;
|
|
@@ -9,6 +10,7 @@ const MAX_ATTACHMENT_BYTES = 5 * 1024 * 1024;
|
|
|
9
10
|
export class WorkspaceUploadService {
|
|
10
11
|
state;
|
|
11
12
|
onCompleted;
|
|
13
|
+
directHashes = new Map();
|
|
12
14
|
constructor(state, onCompleted) {
|
|
13
15
|
this.state = state;
|
|
14
16
|
this.onCompleted = onCompleted;
|
|
@@ -99,6 +101,44 @@ export class WorkspaceUploadService {
|
|
|
99
101
|
upload.receivedBytes += bytes.length;
|
|
100
102
|
return this.status(upload.id);
|
|
101
103
|
}
|
|
104
|
+
async createDirect(workspace, input) {
|
|
105
|
+
const normalized = input.path.replaceAll('\\', '/');
|
|
106
|
+
const status = await this.create(workspace, {
|
|
107
|
+
name: posix.basename(normalized),
|
|
108
|
+
size: input.size,
|
|
109
|
+
overwrite: input.overwrite,
|
|
110
|
+
...(input.composerAttachment === undefined
|
|
111
|
+
? { directory: posix.dirname(normalized) }
|
|
112
|
+
: {
|
|
113
|
+
composerAttachmentId: input.composerAttachment.id,
|
|
114
|
+
mime: input.composerAttachment.mime
|
|
115
|
+
})
|
|
116
|
+
});
|
|
117
|
+
this.directHashes.set(status.uploadId, input.sha256);
|
|
118
|
+
return status;
|
|
119
|
+
}
|
|
120
|
+
async writeDirect(uploadId, bytes) {
|
|
121
|
+
const status = await this.write({
|
|
122
|
+
uploadId,
|
|
123
|
+
offset: this.status(uploadId).receivedBytes,
|
|
124
|
+
dataBase64: Buffer.from(bytes).toString('base64')
|
|
125
|
+
});
|
|
126
|
+
return status.receivedBytes;
|
|
127
|
+
}
|
|
128
|
+
async completeDirect(uploadId, size, sha256) {
|
|
129
|
+
const upload = this.require(uploadId);
|
|
130
|
+
const expected = this.directHashes.get(uploadId);
|
|
131
|
+
if (size !== upload.size || sha256 !== expected)
|
|
132
|
+
throw new Error('UPLOAD_HASH_INVALID');
|
|
133
|
+
await upload.handle.sync();
|
|
134
|
+
const actual = await fileSha256(upload.temporaryPath);
|
|
135
|
+
if (actual !== expected) {
|
|
136
|
+
await this.cancel(uploadId);
|
|
137
|
+
throw new Error('UPLOAD_HASH_INVALID');
|
|
138
|
+
}
|
|
139
|
+
await this.complete(uploadId);
|
|
140
|
+
this.directHashes.delete(uploadId);
|
|
141
|
+
}
|
|
102
142
|
status(uploadId) {
|
|
103
143
|
const upload = this.require(uploadId);
|
|
104
144
|
return {
|
|
@@ -163,8 +203,10 @@ export class WorkspaceUploadService {
|
|
|
163
203
|
if (typeof uploadId !== 'string')
|
|
164
204
|
throw new Error('UPLOAD_NOT_FOUND');
|
|
165
205
|
const active = this.state.uploads.get(uploadId);
|
|
166
|
-
if (active !== undefined)
|
|
206
|
+
if (active !== undefined) {
|
|
207
|
+
this.directHashes.delete(active.id);
|
|
167
208
|
return this.remove(active);
|
|
209
|
+
}
|
|
168
210
|
const attachment = this.state.composerAttachments.get(uploadId);
|
|
169
211
|
if (attachment === undefined)
|
|
170
212
|
throw new Error('UPLOAD_NOT_FOUND');
|
|
@@ -204,6 +246,7 @@ export class WorkspaceUploadService {
|
|
|
204
246
|
for (const attachment of this.state.composerAttachments.values())
|
|
205
247
|
await this.removeCompleted(attachment);
|
|
206
248
|
this.state.composerAttachments.clear();
|
|
249
|
+
this.directHashes.clear();
|
|
207
250
|
}
|
|
208
251
|
require(id) {
|
|
209
252
|
if (typeof id !== 'string')
|
|
@@ -215,6 +258,7 @@ export class WorkspaceUploadService {
|
|
|
215
258
|
}
|
|
216
259
|
async remove(upload) {
|
|
217
260
|
this.state.uploads.delete(upload.id);
|
|
261
|
+
this.directHashes.delete(upload.id);
|
|
218
262
|
clearTimeout(upload.timer);
|
|
219
263
|
await upload.handle.close().catch(() => undefined);
|
|
220
264
|
await rm(upload.temporaryPath, { force: true }).catch(() => undefined);
|
|
@@ -252,6 +296,12 @@ export class WorkspaceUploadService {
|
|
|
252
296
|
};
|
|
253
297
|
}
|
|
254
298
|
}
|
|
299
|
+
async function fileSha256(path) {
|
|
300
|
+
const hash = createHash('sha256');
|
|
301
|
+
for await (const chunk of createReadStream(path))
|
|
302
|
+
hash.update(chunk);
|
|
303
|
+
return hash.digest('hex');
|
|
304
|
+
}
|
|
255
305
|
async function safeDirectory(path) {
|
|
256
306
|
const value = await lstat(path);
|
|
257
307
|
if (!value.isDirectory() || value.isSymbolicLink())
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { listGitHistoryRefs, openGitHistoryFileContent, readGitHistory, readGitHistoryFileDiff, readGitHistoryFileContentRange, readGitHistoryFiles, readCurrentChangeDiff, readRestrictedDiff, resolveGitRepository, resolveGitRepositoryTarget, restoreCurrentChange } from '../workspace.js';
|
|
2
3
|
import { WorkspaceContentSearchService } from './workspace-content-search-service.js';
|
|
3
4
|
import { WorkspaceWatchController } from './workspace-watch-controller.js';
|
|
4
5
|
import { WorkspaceWatchSnapshotService } from './workspace-watch-snapshot-service.js';
|
|
@@ -25,6 +26,7 @@ export class WorkspaceWorkbenchService {
|
|
|
25
26
|
'workspace.file.read': (data) => this.readFile(record(data)),
|
|
26
27
|
'workspace.file.write': (data) => this.writeFile(record(data)),
|
|
27
28
|
'workspace.file.content.read': (data) => this.readFileContent(record(data)),
|
|
29
|
+
'workspace.file.direct.metadata': (data) => this.directFileMetadata(record(data)),
|
|
28
30
|
'workspace.files.search': (data) => this.searchFiles(record(data)),
|
|
29
31
|
'workspace.files.mutate': (data) => this.mutateFile(record(data)),
|
|
30
32
|
'workspace.content.search': (data) => this.searchContent(record(data)),
|
|
@@ -44,6 +46,7 @@ export class WorkspaceWorkbenchService {
|
|
|
44
46
|
'workspace.git.commit.files': (data) => this.gitCommitFiles(record(data)),
|
|
45
47
|
'workspace.git.commit.file.diff': (data) => this.gitCommitFileDiff(record(data)),
|
|
46
48
|
'workspace.git.commit.file.content.read': (data) => this.gitCommitFileContent(record(data)),
|
|
49
|
+
'workspace.git.commit.file.direct.metadata': (data) => this.gitCommitFileDirectMetadata(record(data)),
|
|
47
50
|
'workspace.diff': (data) => this.diff(record(data))
|
|
48
51
|
};
|
|
49
52
|
}
|
|
@@ -123,6 +126,15 @@ export class WorkspaceWorkbenchService {
|
|
|
123
126
|
file: await this.options.files.readContent(workspace, input, repository)
|
|
124
127
|
};
|
|
125
128
|
}
|
|
129
|
+
async directFileMetadata(input) {
|
|
130
|
+
if (typeof input.workspaceId !== 'string')
|
|
131
|
+
throw new Error('WORKSPACE_NOT_FOUND');
|
|
132
|
+
const workspace = this.options.requireWorkspace(input.workspaceId);
|
|
133
|
+
const repository = await this.fileRepository(workspace, input);
|
|
134
|
+
return {
|
|
135
|
+
file: await this.options.files.directMetadata(workspace, input, repository)
|
|
136
|
+
};
|
|
137
|
+
}
|
|
126
138
|
async changeDiff(input) {
|
|
127
139
|
if (typeof input.workspaceId !== 'string' ||
|
|
128
140
|
typeof input.path !== 'string' ||
|
|
@@ -217,6 +229,76 @@ export class WorkspaceWorkbenchService {
|
|
|
217
229
|
file: await readGitHistoryFileContentRange(await this.gitRepository(input), input.commit, input.path, input.offset, input.limit, this.refs(input))
|
|
218
230
|
};
|
|
219
231
|
}
|
|
232
|
+
async gitCommitFileDirectMetadata(input) {
|
|
233
|
+
return {
|
|
234
|
+
file: await this.gitDirectMetadata(input)
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
async directDownload(descriptor) {
|
|
238
|
+
const workspace = this.options.requireWorkspace(descriptor.resource.workspaceId);
|
|
239
|
+
if (descriptor.resource.kind === 'WORKSPACE_FILE') {
|
|
240
|
+
const input = {
|
|
241
|
+
workspaceId: workspace.id,
|
|
242
|
+
path: descriptor.resource.path,
|
|
243
|
+
...(descriptor.resource.worktreePath === undefined
|
|
244
|
+
? {}
|
|
245
|
+
: { worktreePath: descriptor.resource.worktreePath })
|
|
246
|
+
};
|
|
247
|
+
return {
|
|
248
|
+
offset: descriptor.resumeOffset ?? 0,
|
|
249
|
+
chunks: this.options.files.readDirect(workspace, {
|
|
250
|
+
path: descriptor.resource.path,
|
|
251
|
+
size: descriptor.size,
|
|
252
|
+
sha256: descriptor.sha256
|
|
253
|
+
}, await this.fileRepository(workspace, input))
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
const input = {
|
|
257
|
+
workspaceId: workspace.id,
|
|
258
|
+
path: descriptor.resource.path,
|
|
259
|
+
commit: descriptor.resource.commit,
|
|
260
|
+
...(descriptor.resource.repositoryPath === undefined
|
|
261
|
+
? {}
|
|
262
|
+
: { repositoryPath: descriptor.resource.repositoryPath }),
|
|
263
|
+
...(descriptor.resource.worktreePath === undefined
|
|
264
|
+
? {}
|
|
265
|
+
: { worktreePath: descriptor.resource.worktreePath })
|
|
266
|
+
};
|
|
267
|
+
const repository = await this.gitRepository(input);
|
|
268
|
+
const content = await openGitHistoryFileContent(repository, descriptor.resource.commit, descriptor.resource.path, this.refs(input));
|
|
269
|
+
if (content.size !== descriptor.size)
|
|
270
|
+
throw new Error('DIRECT_FILE_REVISION_CHANGED');
|
|
271
|
+
return {
|
|
272
|
+
offset: descriptor.resumeOffset ?? 0,
|
|
273
|
+
chunks: (async function* () {
|
|
274
|
+
const hash = createHash('sha256');
|
|
275
|
+
let offset = 0;
|
|
276
|
+
for await (const bytes of content.chunks) {
|
|
277
|
+
hash.update(bytes);
|
|
278
|
+
offset += bytes.length;
|
|
279
|
+
if (bytes.length > 0)
|
|
280
|
+
yield bytes;
|
|
281
|
+
}
|
|
282
|
+
if (offset !== descriptor.size || hash.digest('hex') !== descriptor.sha256)
|
|
283
|
+
throw new Error('DIRECT_FILE_REVISION_CHANGED');
|
|
284
|
+
})()
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
async gitDirectMetadata(input) {
|
|
288
|
+
const repository = await this.gitRepository(input);
|
|
289
|
+
if (typeof input.commit !== 'string' || typeof input.path !== 'string')
|
|
290
|
+
throw new Error('GIT_HISTORY_COMMIT_INVALID');
|
|
291
|
+
const content = await openGitHistoryFileContent(repository, input.commit, input.path, this.refs(input));
|
|
292
|
+
const hash = createHash('sha256');
|
|
293
|
+
let offset = 0;
|
|
294
|
+
for await (const bytes of content.chunks) {
|
|
295
|
+
hash.update(bytes);
|
|
296
|
+
offset += bytes.length;
|
|
297
|
+
}
|
|
298
|
+
if (offset !== content.size)
|
|
299
|
+
throw new Error('DIRECT_FILE_REVISION_CHANGED');
|
|
300
|
+
return { size: content.size, sha256: hash.digest('hex') };
|
|
301
|
+
}
|
|
220
302
|
async diff(input) {
|
|
221
303
|
if (typeof input.workspaceId !== 'string' ||
|
|
222
304
|
typeof input.fromRef !== 'string' ||
|
package/dist/workspace.js
CHANGED
|
@@ -1510,6 +1510,67 @@ export async function readGitHistoryFileContentRange(repository, commit, request
|
|
|
1510
1510
|
nextOffset: nextOffset < size ? nextOffset : null
|
|
1511
1511
|
};
|
|
1512
1512
|
}
|
|
1513
|
+
export async function openGitHistoryFileContent(repository, commit, requestedPath, refs) {
|
|
1514
|
+
await assertHistoryCommitReachable(repository, commit, refs);
|
|
1515
|
+
const path = safeGitWorkspacePath(requestedPath);
|
|
1516
|
+
const object = `${commit}:${path}`;
|
|
1517
|
+
const sizeResult = await runReadOnlyGit(repository, ['cat-file', '-s', object]);
|
|
1518
|
+
const size = Number(sizeResult.text.trim());
|
|
1519
|
+
if (!Number.isSafeInteger(size) || size < 0)
|
|
1520
|
+
throw new Error('FILE_RANGE_INVALID');
|
|
1521
|
+
return {
|
|
1522
|
+
path,
|
|
1523
|
+
size,
|
|
1524
|
+
chunks: (async function* () {
|
|
1525
|
+
const child = spawn('git', ['cat-file', 'blob', object], {
|
|
1526
|
+
cwd: repository,
|
|
1527
|
+
shell: false,
|
|
1528
|
+
windowsHide: true,
|
|
1529
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
1530
|
+
env: {
|
|
1531
|
+
...process.env,
|
|
1532
|
+
...gitOperationEnvironment.getStore(),
|
|
1533
|
+
GIT_OPTIONAL_LOCKS: '0'
|
|
1534
|
+
}
|
|
1535
|
+
});
|
|
1536
|
+
const errors = [];
|
|
1537
|
+
let errorSize = 0;
|
|
1538
|
+
let closed = false;
|
|
1539
|
+
const exit = new Promise((resolveExit) => {
|
|
1540
|
+
child.stderr.on('data', (chunk) => {
|
|
1541
|
+
const remaining = Math.max(0, 4096 - errorSize);
|
|
1542
|
+
if (remaining === 0)
|
|
1543
|
+
return;
|
|
1544
|
+
const retained = chunk.subarray(0, remaining);
|
|
1545
|
+
errors.push(retained);
|
|
1546
|
+
errorSize += retained.length;
|
|
1547
|
+
});
|
|
1548
|
+
child.once('error', (error) => {
|
|
1549
|
+
closed = true;
|
|
1550
|
+
resolveExit({ code: 1, stderr: error.message });
|
|
1551
|
+
});
|
|
1552
|
+
child.once('close', (code) => {
|
|
1553
|
+
closed = true;
|
|
1554
|
+
resolveExit({
|
|
1555
|
+
code: code ?? 1,
|
|
1556
|
+
stderr: Buffer.concat(errors).toString('utf8')
|
|
1557
|
+
});
|
|
1558
|
+
});
|
|
1559
|
+
});
|
|
1560
|
+
try {
|
|
1561
|
+
for await (const chunk of child.stdout)
|
|
1562
|
+
yield new Uint8Array(chunk);
|
|
1563
|
+
const result = await exit;
|
|
1564
|
+
if (result.code !== 0)
|
|
1565
|
+
throw new GitCommandError(result.code, result.stderr);
|
|
1566
|
+
}
|
|
1567
|
+
finally {
|
|
1568
|
+
if (!closed)
|
|
1569
|
+
child.kill('SIGKILL');
|
|
1570
|
+
}
|
|
1571
|
+
})()
|
|
1572
|
+
};
|
|
1573
|
+
}
|
|
1513
1574
|
export async function readCurrentChanges(workspacePath) {
|
|
1514
1575
|
return (await readCurrentChangesSummary(workspacePath)).changes;
|
|
1515
1576
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@myagentroam/node",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.68",
|
|
4
4
|
"description": "MyAgentRoam Node runtime CLI.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -24,11 +24,12 @@
|
|
|
24
24
|
"@modelcontextprotocol/sdk": "1.30.0",
|
|
25
25
|
"@opencode-ai/sdk": "1.18.20",
|
|
26
26
|
"minimatch": "^10.2.6",
|
|
27
|
+
"node-datachannel": "0.33.4",
|
|
27
28
|
"node-pty": "1.1.0",
|
|
28
29
|
"ws": "^8.21.3",
|
|
29
30
|
"zod": "4.4.3",
|
|
30
|
-
"@myagentroam/agent": "0.9.
|
|
31
|
-
"@myagentroam/protocol": "0.9.
|
|
31
|
+
"@myagentroam/agent": "0.9.68",
|
|
32
|
+
"@myagentroam/protocol": "0.9.68"
|
|
32
33
|
},
|
|
33
34
|
"devDependencies": {
|
|
34
35
|
"@types/ws": "^8.18.1"
|