@myagentroam/node 0.9.133 → 0.9.135
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 +5 -0
- package/dist/connector.js +9 -0
- package/dist/node-execution-environment.js +16 -3
- package/dist/runner/mar-agent/runtime-model-factory.js +17 -7
- package/dist/runtime-state.js +2 -2
- package/dist/service/chatgpt-login-service.js +345 -0
- package/dist/service/external-session-resume-service.js +2 -0
- package/dist/service/native-session-projection-service.js +2 -0
- package/package.json +5 -5
package/dist/capabilities.js
CHANGED
|
@@ -9,6 +9,8 @@ const runtimeCredentialCapability = () => ({
|
|
|
9
9
|
apiVersion: 1,
|
|
10
10
|
websocket: true,
|
|
11
11
|
fastMode: true,
|
|
12
|
+
serviceTiers: ['auto', 'default', 'flex', 'priority', 'fast', 'ultrafast'],
|
|
13
|
+
namespacedTools: true,
|
|
12
14
|
httpVersions: ['1.1', '2']
|
|
13
15
|
});
|
|
14
16
|
const workspaceDirectTransferCapability = () => ({
|
|
@@ -52,6 +54,7 @@ export function unavailableCapabilities() {
|
|
|
52
54
|
openCode: { available: false },
|
|
53
55
|
marAgent: { available: true, version: nodeAppVersion },
|
|
54
56
|
marAgentRuntimeCredentials: runtimeCredentialCapability(),
|
|
57
|
+
chatgptLogin: { apiVersion: 1, loopback: true },
|
|
55
58
|
workspaceDirectTransfer: workspaceDirectTransferCapability(),
|
|
56
59
|
terminalDirect: terminalDirectCapability()
|
|
57
60
|
};
|
|
@@ -86,6 +89,7 @@ export function detectCapabilities(commands = {}) {
|
|
|
86
89
|
},
|
|
87
90
|
marAgent: { available: true, version: nodeAppVersion },
|
|
88
91
|
marAgentRuntimeCredentials: runtimeCredentialCapability(),
|
|
92
|
+
chatgptLogin: { apiVersion: 1, loopback: true },
|
|
89
93
|
workspaceDirectTransfer: workspaceDirectTransferCapability(),
|
|
90
94
|
terminalDirect: terminalDirectCapability()
|
|
91
95
|
};
|
|
@@ -124,6 +128,7 @@ export async function detectCapabilitiesAsync(commands = {}) {
|
|
|
124
128
|
},
|
|
125
129
|
marAgent: { available: true, version: nodeAppVersion },
|
|
126
130
|
marAgentRuntimeCredentials: runtimeCredentialCapability(),
|
|
131
|
+
chatgptLogin: { apiVersion: 1, loopback: true },
|
|
127
132
|
workspaceDirectTransfer: workspaceDirectTransferCapability(),
|
|
128
133
|
terminalDirect: terminalDirectCapability()
|
|
129
134
|
};
|
package/dist/connector.js
CHANGED
|
@@ -99,6 +99,7 @@ import { safeErrorCode } from './util/safe-error.js';
|
|
|
99
99
|
import { RuntimeCredentialControlService } from './service/runtime-credential-control-service.js';
|
|
100
100
|
import { MarAgentRuntimeModelFactory } from './runner/mar-agent/runtime-model-factory.js';
|
|
101
101
|
import { MarAgentModelTransportTestService } from './service/mar-agent-model-transport-test-service.js';
|
|
102
|
+
import { ChatgptLoginService } from './service/chatgpt-login-service.js';
|
|
102
103
|
const HEARTBEAT_MS = 15_000;
|
|
103
104
|
const CAPABILITY_REFRESH_MS = 60_000;
|
|
104
105
|
const RETRY_MIN_MS = 500;
|
|
@@ -170,6 +171,11 @@ export class NodeConnector {
|
|
|
170
171
|
marAgentModelTransportTests = new MarAgentModelTransportTestService({
|
|
171
172
|
modelFactory: this.marAgentRuntimeModels
|
|
172
173
|
});
|
|
174
|
+
chatgptLogins = new ChatgptLoginService(() => {
|
|
175
|
+
if (!this.config)
|
|
176
|
+
throw new Error('NODE_CONFIG_UNAVAILABLE');
|
|
177
|
+
return nodeDataDirectory(this.config, this.configPath);
|
|
178
|
+
});
|
|
173
179
|
/** @internal Existing contract tests inject their socket through this boundary. */
|
|
174
180
|
set socket(value) {
|
|
175
181
|
this.controlChannel.replaceSocketForTesting(value);
|
|
@@ -1233,6 +1239,7 @@ export class NodeConnector {
|
|
|
1233
1239
|
onClose: (code, reason) => {
|
|
1234
1240
|
this.directTransferService?.close();
|
|
1235
1241
|
this.runtimeCredentials.disconnected();
|
|
1242
|
+
this.chatgptLogins.close();
|
|
1236
1243
|
this.scheduledRunAuthorization.disconnected();
|
|
1237
1244
|
this.mailNotificationControl.disconnected();
|
|
1238
1245
|
this.mailNotificationMcpServer.revokeAll();
|
|
@@ -1453,6 +1460,7 @@ export class NodeConnector {
|
|
|
1453
1460
|
this.operationRouter.registerAll(this.sessionCommandService.operations());
|
|
1454
1461
|
this.operationRouter.registerAll(this.workspaceQueueWorkbenchService.operations());
|
|
1455
1462
|
this.operationRouter.registerAll(this.marAgentModelTransportTests.operations());
|
|
1463
|
+
this.operationRouter.registerAll(this.chatgptLogins.operations());
|
|
1456
1464
|
this.operationRouter.registerAll(new ConversationHostingEnablementService({
|
|
1457
1465
|
config: () => this.config,
|
|
1458
1466
|
setConfig: (config) => (this.config = config),
|
|
@@ -2009,6 +2017,7 @@ export class NodeConnector {
|
|
|
2009
2017
|
const terminalDrain = this.terminalManager.shutdown();
|
|
2010
2018
|
this.terminalChannel.stop();
|
|
2011
2019
|
this.runtimeCredentials.close();
|
|
2020
|
+
this.chatgptLogins.close();
|
|
2012
2021
|
this.scheduledSessions.stop();
|
|
2013
2022
|
this.scheduledRunAuthorization.close();
|
|
2014
2023
|
this.mailNotificationControl.close();
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
|
-
import { dirname, join, resolve } from 'node:path';
|
|
2
|
+
import { basename, dirname, join, resolve } from 'node:path';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
/** Environment for processes started by this Node, not for the Node service itself. */
|
|
5
5
|
export function nodeExecutionEnvironment(provided = {}, inherited = process.env, options = {}) {
|
|
@@ -8,8 +8,21 @@ export function nodeExecutionEnvironment(provided = {}, inherited = process.env,
|
|
|
8
8
|
environment['MAR_NODE_CONTEXT'] = '1';
|
|
9
9
|
const platform = options.platform ?? process.platform;
|
|
10
10
|
const packageRoot = options.packageRoot ?? resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
11
|
-
const
|
|
12
|
-
|
|
11
|
+
const cliBins = [join(packageRoot, 'node_modules', '.bin')];
|
|
12
|
+
const nodeModules = resolve(packageRoot, '..', '..');
|
|
13
|
+
if (basename(packageRoot) === 'node' &&
|
|
14
|
+
basename(dirname(packageRoot)) === '@myagentroam' &&
|
|
15
|
+
basename(nodeModules) === 'node_modules') {
|
|
16
|
+
// npm prefix installs and pnpm's virtual store place shims beside scoped packages.
|
|
17
|
+
cliBins.push(join(nodeModules, '.bin'));
|
|
18
|
+
const prefix = dirname(nodeModules);
|
|
19
|
+
if (platform === 'win32')
|
|
20
|
+
cliBins.push(prefix);
|
|
21
|
+
else if (basename(prefix) === 'lib')
|
|
22
|
+
cliBins.push(resolve(prefix, '..', 'bin'));
|
|
23
|
+
}
|
|
24
|
+
const cliBin = cliBins.find((bin) => existsSync(join(bin, platform === 'win32' ? 'marnodes.cmd' : 'marnodes')));
|
|
25
|
+
if (cliBin === undefined)
|
|
13
26
|
return environment;
|
|
14
27
|
const separator = platform === 'win32' ? ';' : ':';
|
|
15
28
|
const existingKeys = Object.keys(environment).filter((key) => key.toLowerCase() === 'path');
|
|
@@ -22,6 +22,7 @@ class PendingCredentialResolution {
|
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
24
|
const CREDENTIAL_REFRESH_WINDOW_MS = 60_000;
|
|
25
|
+
const CREDENTIAL_REFRESH_RETRY_MS = 30_000;
|
|
25
26
|
export class MarAgentRuntimeModelFactory {
|
|
26
27
|
control;
|
|
27
28
|
now;
|
|
@@ -64,7 +65,12 @@ export class MarAgentRuntimeModelFactory {
|
|
|
64
65
|
titleGeneration: configuration.titleGeneration,
|
|
65
66
|
codeMode: configuration.codeMode,
|
|
66
67
|
highDensityCompaction: configuration.highDensityCompaction,
|
|
67
|
-
...(configuration.
|
|
68
|
+
...(configuration.serviceTier === undefined
|
|
69
|
+
? {}
|
|
70
|
+
: { serviceTier: configuration.serviceTier }),
|
|
71
|
+
...(configuration.serviceTier === undefined && configuration.fastMode === true
|
|
72
|
+
? { fastMode: true }
|
|
73
|
+
: {}),
|
|
68
74
|
hostedWebSearch: configuration.hostedWebSearch,
|
|
69
75
|
...(imageGeneration === undefined ? {} : { imageGeneration }),
|
|
70
76
|
...(configuration.responsesTransport === undefined
|
|
@@ -73,6 +79,7 @@ export class MarAgentRuntimeModelFactory {
|
|
|
73
79
|
...(configuration.responsesEncoding === undefined
|
|
74
80
|
? {}
|
|
75
81
|
: { responsesEncoding: configuration.responsesEncoding }),
|
|
82
|
+
responsesToolNamespace: configuration.responsesToolNamespace,
|
|
76
83
|
responsesPreviousResponseId: configuration.responsesPreviousResponseId,
|
|
77
84
|
enabled: configuration.enabled
|
|
78
85
|
};
|
|
@@ -229,12 +236,12 @@ class ServerRuntimeCredentialProvider {
|
|
|
229
236
|
if (this.state.pending !== pending)
|
|
230
237
|
return;
|
|
231
238
|
delete this.state.pending;
|
|
232
|
-
scheduleRefresh(this.state, this.now);
|
|
239
|
+
scheduleRefresh(this.state, this.now, CREDENTIAL_REFRESH_RETRY_MS);
|
|
233
240
|
}, () => {
|
|
234
241
|
if (this.state.pending !== pending)
|
|
235
242
|
return;
|
|
236
243
|
delete this.state.pending;
|
|
237
|
-
scheduleRefresh(this.state, this.now,
|
|
244
|
+
scheduleRefresh(this.state, this.now, CREDENTIAL_REFRESH_RETRY_MS);
|
|
238
245
|
});
|
|
239
246
|
return pending;
|
|
240
247
|
}
|
|
@@ -296,20 +303,23 @@ function sameRuntimeCredentialTicket(first, second) {
|
|
|
296
303
|
first.allowedModelIds.length === second.allowedModelIds.length &&
|
|
297
304
|
first.allowedModelIds.every((modelId, index) => modelId === second.allowedModelIds[index]));
|
|
298
305
|
}
|
|
299
|
-
function scheduleRefresh(state, now,
|
|
306
|
+
function scheduleRefresh(state, now, minimumDelayMs = 0) {
|
|
300
307
|
clearRefreshTimer(state);
|
|
301
308
|
if (state.released || state.pending !== undefined || state.providers.size === 0)
|
|
302
309
|
return;
|
|
310
|
+
const timestamp = now();
|
|
311
|
+
if (state.ticket.expiresAt <= timestamp)
|
|
312
|
+
return;
|
|
303
313
|
const expiresAt = Math.min(state.ticket.expiresAt, state.credential?.expiresAt ?? Number.POSITIVE_INFINITY);
|
|
304
|
-
const delay = expiresAt -
|
|
305
|
-
if (
|
|
314
|
+
const delay = Math.max(minimumDelayMs, expiresAt - timestamp - CREDENTIAL_REFRESH_WINDOW_MS, 0);
|
|
315
|
+
if (timestamp + delay > state.ticket.expiresAt)
|
|
306
316
|
return;
|
|
307
317
|
const timer = setTimeout(() => {
|
|
308
318
|
if (state.refreshTimer === timer)
|
|
309
319
|
delete state.refreshTimer;
|
|
310
320
|
const provider = state.providers.values().next().value;
|
|
311
321
|
provider?.refreshAutomatically();
|
|
312
|
-
},
|
|
322
|
+
}, delay);
|
|
313
323
|
timer.unref();
|
|
314
324
|
state.refreshTimer = timer;
|
|
315
325
|
}
|
package/dist/runtime-state.js
CHANGED
|
@@ -79,8 +79,8 @@ export class NodeRuntimeState {
|
|
|
79
79
|
// A first user prompt is only a process-local temporary title. A
|
|
80
80
|
// Runner/native title can replace it on the next discovery refresh.
|
|
81
81
|
titleSource: customTitle === null ? 'AUTO' : 'CUSTOM',
|
|
82
|
-
lastActivityAt: now,
|
|
83
|
-
createdAt: now
|
|
82
|
+
lastActivityAt: input.lastActivityAt ?? now,
|
|
83
|
+
createdAt: input.createdAt ?? now
|
|
84
84
|
};
|
|
85
85
|
this.sessions.set(value.id, value);
|
|
86
86
|
return value;
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
import { createHash, randomBytes, randomUUID } from 'node:crypto';
|
|
2
|
+
import { createServer } from 'node:http';
|
|
3
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { requestHttp } from '@myagentroam/agent/host/network';
|
|
6
|
+
import { CHATGPT_ISSUER, CHATGPT_RESOURCE, CHATGPT_TOKEN_ENDPOINT, CHATGPT_JWKS_ENDPOINT, CHATGPT_REQUESTED_SCOPE, chatgptLoginStartSchema, chatgptTokenResponseSchema, verifyChatgptIdentity } from '@myagentroam/protocol';
|
|
7
|
+
const TTL_MS = 10 * 60_000;
|
|
8
|
+
/** Temporary, loopback-only OAuth provider. Only the Server may consume token results. */
|
|
9
|
+
export class ChatgptLoginService {
|
|
10
|
+
dataDirectory;
|
|
11
|
+
request;
|
|
12
|
+
attempts = new Map();
|
|
13
|
+
starting = new Map();
|
|
14
|
+
hostIdFlight;
|
|
15
|
+
constructor(dataDirectory, request = (url, init, proxy, signal) => requestHttp(url, { request: init, httpVersion: '1.1', ...(proxy ? { socks5Url: proxy } : {}) }, signal ?? AbortSignal.timeout(30_000), 30_000)) {
|
|
16
|
+
this.dataDirectory = dataDirectory;
|
|
17
|
+
this.request = request;
|
|
18
|
+
}
|
|
19
|
+
operations() {
|
|
20
|
+
return {
|
|
21
|
+
'chatgpt.login.start': (raw) => this.start(raw),
|
|
22
|
+
'chatgpt.login.status': (raw) => this.status(raw),
|
|
23
|
+
'chatgpt.login.ack': (raw) => this.remove(raw),
|
|
24
|
+
'chatgpt.login.cancel': (raw) => this.remove(raw)
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
async start(raw) {
|
|
28
|
+
const { data, userId, session } = this.context(raw);
|
|
29
|
+
const parsed = chatgptLoginStartSchema.safeParse(data);
|
|
30
|
+
if (!parsed.success)
|
|
31
|
+
throw new Error('CHATGPT_LOGIN_INPUT_INVALID');
|
|
32
|
+
const input = parsed.data;
|
|
33
|
+
if (this.attempts.has(input.attemptId))
|
|
34
|
+
return this.status(raw);
|
|
35
|
+
const existing = this.starting.get(input.attemptId);
|
|
36
|
+
if (existing) {
|
|
37
|
+
if (existing.userId !== userId || existing.session !== session)
|
|
38
|
+
throw new Error('CHATGPT_LOGIN_FORBIDDEN');
|
|
39
|
+
await existing.flight;
|
|
40
|
+
return this.status(raw);
|
|
41
|
+
}
|
|
42
|
+
if (this.attempts.size + this.starting.size >= 64)
|
|
43
|
+
throw new Error('CHATGPT_LOGIN_LIMIT_REACHED');
|
|
44
|
+
if (input.socks5Url !== undefined && new URL(input.socks5Url).protocol !== 'socks5:')
|
|
45
|
+
throw new Error('CHATGPT_LOGIN_INPUT_INVALID');
|
|
46
|
+
const pending = { userId, session, cancelled: false };
|
|
47
|
+
this.starting.set(input.attemptId, pending);
|
|
48
|
+
pending.flight = this.openAttempt(input, pending);
|
|
49
|
+
try {
|
|
50
|
+
await pending.flight;
|
|
51
|
+
return this.status(raw);
|
|
52
|
+
}
|
|
53
|
+
finally {
|
|
54
|
+
if (this.starting.get(input.attemptId) === pending)
|
|
55
|
+
this.starting.delete(input.attemptId);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
async openAttempt(input, pending) {
|
|
59
|
+
const hostId = await this.hostId();
|
|
60
|
+
if (pending.cancelled)
|
|
61
|
+
throw new Error('CHATGPT_LOGIN_CANCELLED');
|
|
62
|
+
const server = createServer((req, res) => {
|
|
63
|
+
void this.handle(input.attemptId, req, res).catch(() => {
|
|
64
|
+
if (!res.headersSent)
|
|
65
|
+
res.writeHead(500);
|
|
66
|
+
res.end('Login failed.');
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
server.maxHeadersCount = 32;
|
|
70
|
+
server.headersTimeout = 10_000;
|
|
71
|
+
server.requestTimeout = 10_000;
|
|
72
|
+
await new Promise((resolve, reject) => {
|
|
73
|
+
server.once('error', reject);
|
|
74
|
+
server.listen(0, '127.0.0.1', () => {
|
|
75
|
+
server.removeListener('error', reject);
|
|
76
|
+
resolve();
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
const address = server.address();
|
|
80
|
+
if (pending.cancelled) {
|
|
81
|
+
server.close();
|
|
82
|
+
server.closeAllConnections();
|
|
83
|
+
throw new Error('CHATGPT_LOGIN_CANCELLED');
|
|
84
|
+
}
|
|
85
|
+
if (address === null || typeof address === 'string') {
|
|
86
|
+
server.close();
|
|
87
|
+
throw new Error('CHATGPT_LISTENER_FAILED');
|
|
88
|
+
}
|
|
89
|
+
const attempt = {
|
|
90
|
+
id: input.attemptId,
|
|
91
|
+
userId: pending.userId,
|
|
92
|
+
session: pending.session,
|
|
93
|
+
nonce: input.nonce,
|
|
94
|
+
state: randomBytes(32).toString('base64url'),
|
|
95
|
+
entry: randomBytes(32).toString('base64url'),
|
|
96
|
+
verifier: randomBytes(48).toString('base64url'),
|
|
97
|
+
clientId: input.clientId,
|
|
98
|
+
subject: input.subject,
|
|
99
|
+
proxy: input.socks5Url,
|
|
100
|
+
hostId,
|
|
101
|
+
server,
|
|
102
|
+
expiresAt: Date.now() + TTL_MS,
|
|
103
|
+
redirectUri: `http://127.0.0.1:${address.port}/auth/callback`,
|
|
104
|
+
abort: new AbortController(),
|
|
105
|
+
started: false,
|
|
106
|
+
exchanging: false,
|
|
107
|
+
timer: setTimeout(() => this.clear(input.attemptId), TTL_MS)
|
|
108
|
+
};
|
|
109
|
+
attempt.timer.unref();
|
|
110
|
+
this.attempts.set(attempt.id, attempt);
|
|
111
|
+
}
|
|
112
|
+
status(raw) {
|
|
113
|
+
const attempt = this.owned(raw);
|
|
114
|
+
if (attempt.result !== undefined)
|
|
115
|
+
return { status: 'SUCCEEDED', result: attempt.result };
|
|
116
|
+
if (attempt.error !== undefined)
|
|
117
|
+
return { status: 'FAILED', code: attempt.error };
|
|
118
|
+
return {
|
|
119
|
+
status: 'PENDING',
|
|
120
|
+
verificationUri: attempt.redirectUri.replace('/auth/callback', `/auth/start?ticket=${attempt.entry}`),
|
|
121
|
+
expiresAt: attempt.expiresAt
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
async remove(raw) {
|
|
125
|
+
const { data, userId, session } = this.context(raw);
|
|
126
|
+
const id = data['attemptId'];
|
|
127
|
+
const pending = typeof id === 'string' ? this.starting.get(id) : undefined;
|
|
128
|
+
if (pending) {
|
|
129
|
+
if (pending.userId !== userId || pending.session !== session)
|
|
130
|
+
throw new Error('CHATGPT_LOGIN_FORBIDDEN');
|
|
131
|
+
pending.cancelled = true;
|
|
132
|
+
await pending.flight?.catch(() => undefined);
|
|
133
|
+
return { cleared: true };
|
|
134
|
+
}
|
|
135
|
+
const attempt = this.owned(raw);
|
|
136
|
+
this.clear(attempt.id);
|
|
137
|
+
return { cleared: true };
|
|
138
|
+
}
|
|
139
|
+
close() {
|
|
140
|
+
for (const pending of this.starting.values())
|
|
141
|
+
pending.cancelled = true;
|
|
142
|
+
this.starting.clear();
|
|
143
|
+
for (const id of this.attempts.keys())
|
|
144
|
+
this.clear(id);
|
|
145
|
+
}
|
|
146
|
+
clear(id) {
|
|
147
|
+
const attempt = this.attempts.get(id);
|
|
148
|
+
if (!attempt)
|
|
149
|
+
return;
|
|
150
|
+
this.attempts.delete(id);
|
|
151
|
+
clearTimeout(attempt.timer);
|
|
152
|
+
attempt.abort.abort();
|
|
153
|
+
attempt.server.close();
|
|
154
|
+
attempt.server.closeAllConnections();
|
|
155
|
+
attempt.result = undefined;
|
|
156
|
+
attempt.verifier = '';
|
|
157
|
+
attempt.entry = '';
|
|
158
|
+
}
|
|
159
|
+
context(raw) {
|
|
160
|
+
if (raw === null || typeof raw !== 'object' || Array.isArray(raw))
|
|
161
|
+
throw new Error('CHATGPT_LOGIN_INPUT_INVALID');
|
|
162
|
+
const data = { ...raw };
|
|
163
|
+
const userId = data['__requestUserId'], session = data['__workspaceWatchSessionHash'];
|
|
164
|
+
delete data['__requestUserId'];
|
|
165
|
+
delete data['__workspaceWatchSessionHash'];
|
|
166
|
+
if (!Number.isSafeInteger(userId) ||
|
|
167
|
+
userId < 1 ||
|
|
168
|
+
typeof session !== 'string' ||
|
|
169
|
+
session.length < 1)
|
|
170
|
+
throw new Error('CHATGPT_LOGIN_FORBIDDEN');
|
|
171
|
+
return { data, userId: userId, session };
|
|
172
|
+
}
|
|
173
|
+
owned(raw) {
|
|
174
|
+
const { data, userId, session } = this.context(raw);
|
|
175
|
+
const id = data['attemptId'];
|
|
176
|
+
const attempt = typeof id === 'string' ? this.attempts.get(id) : undefined;
|
|
177
|
+
if (!attempt ||
|
|
178
|
+
attempt.userId !== userId ||
|
|
179
|
+
attempt.session !== session ||
|
|
180
|
+
Date.now() >= attempt.expiresAt)
|
|
181
|
+
throw new Error('CHATGPT_LOGIN_NOT_FOUND');
|
|
182
|
+
return attempt;
|
|
183
|
+
}
|
|
184
|
+
hostId() {
|
|
185
|
+
this.hostIdFlight ??= this.loadHostId().catch((error) => {
|
|
186
|
+
this.hostIdFlight = undefined;
|
|
187
|
+
throw error;
|
|
188
|
+
});
|
|
189
|
+
return this.hostIdFlight;
|
|
190
|
+
}
|
|
191
|
+
async loadHostId() {
|
|
192
|
+
const directory = this.dataDirectory(), path = join(directory, 'chatgpt-host-id');
|
|
193
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
194
|
+
try {
|
|
195
|
+
await writeFile(path, `urn:uuid:${randomUUID()}`, { flag: 'wx', mode: 0o600 });
|
|
196
|
+
}
|
|
197
|
+
catch (error) {
|
|
198
|
+
if (error.code !== 'EEXIST')
|
|
199
|
+
throw new Error('CHATGPT_HOST_ID_FAILED');
|
|
200
|
+
}
|
|
201
|
+
const id = await readFile(path, 'utf8');
|
|
202
|
+
if (!/^urn:uuid:[0-9a-f-]{36}$/u.test(id))
|
|
203
|
+
throw new Error('CHATGPT_HOST_ID_INVALID');
|
|
204
|
+
return id;
|
|
205
|
+
}
|
|
206
|
+
async json(url, init, attempt) {
|
|
207
|
+
const response = await this.request(url, { ...init, redirect: 'error' }, attempt.proxy, AbortSignal.any([attempt.abort.signal, AbortSignal.timeout(30_000)]));
|
|
208
|
+
if (!response.ok) {
|
|
209
|
+
await response.body?.cancel();
|
|
210
|
+
throw new Error('CHATGPT_TOKEN_EXCHANGE_FAILED');
|
|
211
|
+
}
|
|
212
|
+
const reader = response.body?.getReader();
|
|
213
|
+
if (!reader)
|
|
214
|
+
throw new Error('CHATGPT_TOKEN_RESPONSE_INVALID');
|
|
215
|
+
let length = 0;
|
|
216
|
+
const chunks = [];
|
|
217
|
+
try {
|
|
218
|
+
for (;;) {
|
|
219
|
+
const { done, value } = await reader.read();
|
|
220
|
+
if (done)
|
|
221
|
+
break;
|
|
222
|
+
length += value.length;
|
|
223
|
+
if (length > 524_288)
|
|
224
|
+
throw new Error('CHATGPT_TOKEN_RESPONSE_INVALID');
|
|
225
|
+
chunks.push(value);
|
|
226
|
+
}
|
|
227
|
+
const bytes = new Uint8Array(length);
|
|
228
|
+
let offset = 0;
|
|
229
|
+
for (const chunk of chunks) {
|
|
230
|
+
bytes.set(chunk, offset);
|
|
231
|
+
offset += chunk.length;
|
|
232
|
+
}
|
|
233
|
+
return JSON.parse(new TextDecoder().decode(bytes));
|
|
234
|
+
}
|
|
235
|
+
finally {
|
|
236
|
+
await reader.cancel().catch(() => undefined);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
async handle(id, req, res) {
|
|
240
|
+
res.setHeader('cache-control', 'no-store');
|
|
241
|
+
res.setHeader('referrer-policy', 'no-referrer');
|
|
242
|
+
res.setHeader('content-security-policy', "default-src 'none'; frame-ancestors 'none'");
|
|
243
|
+
const attempt = this.attempts.get(id);
|
|
244
|
+
if (!attempt ||
|
|
245
|
+
req.method !== 'GET' ||
|
|
246
|
+
!req.url ||
|
|
247
|
+
req.url.length > 16_384 ||
|
|
248
|
+
req.headers.host !== new URL(attempt.redirectUri).host ||
|
|
249
|
+
Date.now() >= attempt.expiresAt) {
|
|
250
|
+
res.writeHead(400);
|
|
251
|
+
res.end('Invalid login request.');
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
const url = new URL(req.url, attempt.redirectUri);
|
|
255
|
+
if (url.pathname === '/auth/start' &&
|
|
256
|
+
!attempt.started &&
|
|
257
|
+
url.searchParams.getAll('ticket').length === 1 &&
|
|
258
|
+
url.searchParams.get('ticket') === attempt.entry) {
|
|
259
|
+
attempt.started = true;
|
|
260
|
+
attempt.entry = '';
|
|
261
|
+
const authorize = new URL(`${CHATGPT_ISSUER}/api/accounts/authorize`);
|
|
262
|
+
authorize.search = new URLSearchParams({
|
|
263
|
+
response_type: 'code',
|
|
264
|
+
client_id: attempt.clientId ?? 'dynamic_agent_client',
|
|
265
|
+
ext_agent_host_id: attempt.hostId,
|
|
266
|
+
scope: CHATGPT_REQUESTED_SCOPE,
|
|
267
|
+
resource: CHATGPT_RESOURCE,
|
|
268
|
+
redirect_uri: attempt.redirectUri,
|
|
269
|
+
state: attempt.state,
|
|
270
|
+
nonce: attempt.nonce,
|
|
271
|
+
code_challenge_method: 'S256',
|
|
272
|
+
code_challenge: createHash('sha256').update(attempt.verifier).digest('base64url'),
|
|
273
|
+
...(attempt.clientId ? {} : { agent_name_hint: 'MyAgentRoam' })
|
|
274
|
+
}).toString();
|
|
275
|
+
res.writeHead(302, { location: authorize.href });
|
|
276
|
+
res.end();
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
if (url.pathname !== '/auth/callback' ||
|
|
280
|
+
!attempt.started ||
|
|
281
|
+
attempt.exchanging ||
|
|
282
|
+
attempt.result ||
|
|
283
|
+
attempt.error ||
|
|
284
|
+
url.searchParams.getAll('state').length !== 1 ||
|
|
285
|
+
url.searchParams.get('state') !== attempt.state) {
|
|
286
|
+
res.writeHead(400);
|
|
287
|
+
res.end('Invalid login callback.');
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
attempt.exchanging = true;
|
|
291
|
+
try {
|
|
292
|
+
if (url.searchParams.has('error'))
|
|
293
|
+
throw new Error('CHATGPT_AUTHORIZATION_DENIED');
|
|
294
|
+
const callbackClient = url.searchParams.get('client_id');
|
|
295
|
+
const clientId = callbackClient ?? attempt.clientId;
|
|
296
|
+
const code = url.searchParams.get('code');
|
|
297
|
+
if (!clientId ||
|
|
298
|
+
clientId === 'dynamic_agent_client' ||
|
|
299
|
+
clientId.length > 512 ||
|
|
300
|
+
!code ||
|
|
301
|
+
code.length > 8192 ||
|
|
302
|
+
url.searchParams.getAll('code').length !== 1 ||
|
|
303
|
+
url.searchParams.getAll('client_id').length > 1 ||
|
|
304
|
+
(attempt.clientId && clientId !== attempt.clientId))
|
|
305
|
+
throw new Error('CHATGPT_CALLBACK_INVALID');
|
|
306
|
+
const raw = await this.json(CHATGPT_TOKEN_ENDPOINT, {
|
|
307
|
+
method: 'POST',
|
|
308
|
+
headers: { 'content-type': 'application/x-www-form-urlencoded' },
|
|
309
|
+
body: new URLSearchParams({
|
|
310
|
+
grant_type: 'authorization_code',
|
|
311
|
+
client_id: clientId,
|
|
312
|
+
code,
|
|
313
|
+
code_verifier: attempt.verifier,
|
|
314
|
+
redirect_uri: attempt.redirectUri,
|
|
315
|
+
resource: CHATGPT_RESOURCE
|
|
316
|
+
}).toString()
|
|
317
|
+
}, attempt);
|
|
318
|
+
const parsed = chatgptTokenResponseSchema.safeParse(raw);
|
|
319
|
+
if (!parsed.success || !parsed.data.id_token)
|
|
320
|
+
throw new Error('CHATGPT_TOKEN_RESPONSE_INVALID');
|
|
321
|
+
const jwks = await this.json(CHATGPT_JWKS_ENDPOINT, { method: 'GET' }, attempt);
|
|
322
|
+
await verifyChatgptIdentity(parsed.data.id_token, jwks, {
|
|
323
|
+
clientId,
|
|
324
|
+
nonce: attempt.nonce,
|
|
325
|
+
subject: attempt.subject
|
|
326
|
+
});
|
|
327
|
+
if (this.attempts.get(id) !== attempt || attempt.abort.signal.aborted)
|
|
328
|
+
throw new Error('CHATGPT_LOGIN_CANCELLED');
|
|
329
|
+
attempt.result = { attemptId: id, clientId, hostId: attempt.hostId, tokens: parsed.data };
|
|
330
|
+
res.end('Login completed. Return to MyAgentRoam to save the credential.');
|
|
331
|
+
}
|
|
332
|
+
catch (error) {
|
|
333
|
+
attempt.error =
|
|
334
|
+
error instanceof Error && /^CHATGPT_[A-Z0-9_]+$/u.test(error.message)
|
|
335
|
+
? error.message
|
|
336
|
+
: 'CHATGPT_LOGIN_FAILED';
|
|
337
|
+
res.writeHead(400);
|
|
338
|
+
res.end('Login failed. Return to MyAgentRoam to retry.');
|
|
339
|
+
}
|
|
340
|
+
finally {
|
|
341
|
+
attempt.verifier = '';
|
|
342
|
+
attempt.server.close();
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
@@ -63,6 +63,8 @@ export class ExternalSessionResumeService {
|
|
|
63
63
|
: { externalSessionId: session.externalSessionId }),
|
|
64
64
|
nativeControl: 'MAR_MANAGED',
|
|
65
65
|
cwd: session.cwd,
|
|
66
|
+
createdAt: session.createdAt,
|
|
67
|
+
lastActivityAt: session.lastActivityAt,
|
|
66
68
|
model: configuration.model,
|
|
67
69
|
effort: configuration.effort,
|
|
68
70
|
access: configuration.access,
|
|
@@ -98,6 +98,8 @@ export class NativeSessionProjectionService {
|
|
|
98
98
|
externalSessionId: direct.externalSessionId,
|
|
99
99
|
nativeControl: 'EXTERNAL',
|
|
100
100
|
cwd: direct.cwd,
|
|
101
|
+
createdAt: direct.createdAt,
|
|
102
|
+
lastActivityAt: direct.lastActivityAt,
|
|
101
103
|
...(direct.model === null ? {} : { model: direct.model }),
|
|
102
104
|
...(direct.effort === null ? {} : { effort: direct.effort }),
|
|
103
105
|
...(direct.access === null ? {} : { access: direct.access }),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@myagentroam/node",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.135",
|
|
4
4
|
"description": "MyAgentRoam Node runtime CLI.",
|
|
5
5
|
"license": "Apache-2.0 AND GPL-2.0-or-later",
|
|
6
6
|
"type": "module",
|
|
@@ -34,10 +34,10 @@
|
|
|
34
34
|
"npm": "11.12.1",
|
|
35
35
|
"ws": "^8.21.3",
|
|
36
36
|
"zod": "4.4.3",
|
|
37
|
-
"@myagentroam/marmgr": "0.9.
|
|
38
|
-
"@myagentroam/
|
|
39
|
-
"@myagentroam/protocol": "0.9.
|
|
40
|
-
"@myagentroam/
|
|
37
|
+
"@myagentroam/marmgr": "0.9.118",
|
|
38
|
+
"@myagentroam/code-navigation": "0.9.129",
|
|
39
|
+
"@myagentroam/protocol": "0.9.135",
|
|
40
|
+
"@myagentroam/agent": "0.9.135"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@types/ws": "^8.18.1"
|