@myagentroam/node 0.9.132 → 0.9.134
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 +4 -0
- package/dist/connector.js +9 -0
- package/dist/native-session-history.js +18 -1
- package/dist/node-execution-environment.js +47 -0
- package/dist/runner/abstract-runner.js +10 -12
- package/dist/runner/claude/managed-run-controller.js +5 -1
- package/dist/runner/codex/managed-run-controller.js +3 -2
- package/dist/runner/mar-agent/managed-run-controller.js +5 -1
- package/dist/runner/mar-agent/runtime-model-factory.js +11 -6
- package/dist/runner/mar-agent-runner.js +24 -12
- package/dist/runner/opencode/managed-run-controller.js +4 -3
- package/dist/service/chatgpt-login-service.js +345 -0
- package/dist/service/conversation-history-service.js +10 -8
- package/dist/terminal.js +2 -1
- package/dist/util/runner-native-session-parsers.js +9 -0
- package/package.json +5 -5
package/dist/capabilities.js
CHANGED
|
@@ -9,6 +9,7 @@ const runtimeCredentialCapability = () => ({
|
|
|
9
9
|
apiVersion: 1,
|
|
10
10
|
websocket: true,
|
|
11
11
|
fastMode: true,
|
|
12
|
+
namespacedTools: true,
|
|
12
13
|
httpVersions: ['1.1', '2']
|
|
13
14
|
});
|
|
14
15
|
const workspaceDirectTransferCapability = () => ({
|
|
@@ -52,6 +53,7 @@ export function unavailableCapabilities() {
|
|
|
52
53
|
openCode: { available: false },
|
|
53
54
|
marAgent: { available: true, version: nodeAppVersion },
|
|
54
55
|
marAgentRuntimeCredentials: runtimeCredentialCapability(),
|
|
56
|
+
chatgptLogin: { apiVersion: 1, loopback: true },
|
|
55
57
|
workspaceDirectTransfer: workspaceDirectTransferCapability(),
|
|
56
58
|
terminalDirect: terminalDirectCapability()
|
|
57
59
|
};
|
|
@@ -86,6 +88,7 @@ export function detectCapabilities(commands = {}) {
|
|
|
86
88
|
},
|
|
87
89
|
marAgent: { available: true, version: nodeAppVersion },
|
|
88
90
|
marAgentRuntimeCredentials: runtimeCredentialCapability(),
|
|
91
|
+
chatgptLogin: { apiVersion: 1, loopback: true },
|
|
89
92
|
workspaceDirectTransfer: workspaceDirectTransferCapability(),
|
|
90
93
|
terminalDirect: terminalDirectCapability()
|
|
91
94
|
};
|
|
@@ -124,6 +127,7 @@ export async function detectCapabilitiesAsync(commands = {}) {
|
|
|
124
127
|
},
|
|
125
128
|
marAgent: { available: true, version: nodeAppVersion },
|
|
126
129
|
marAgentRuntimeCredentials: runtimeCredentialCapability(),
|
|
130
|
+
chatgptLogin: { apiVersion: 1, loopback: true },
|
|
127
131
|
workspaceDirectTransfer: workspaceDirectTransferCapability(),
|
|
128
132
|
terminalDirect: terminalDirectCapability()
|
|
129
133
|
};
|
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();
|
|
@@ -433,6 +433,7 @@ async function readTranscript(path, runner, byteLimit, page, targetLines = Numbe
|
|
|
433
433
|
const toolIndexes = new Map();
|
|
434
434
|
const metaRecordIds = new Set();
|
|
435
435
|
const claudeCompactCommandIds = new Set();
|
|
436
|
+
let codexTurnId;
|
|
436
437
|
forEachJsonlRecord(content, (value) => {
|
|
437
438
|
if (derivedTranscriptRecord(runner, value))
|
|
438
439
|
derivedSession = true;
|
|
@@ -444,6 +445,13 @@ async function readTranscript(path, runner, byteLimit, page, targetLines = Numbe
|
|
|
444
445
|
claudeCompactCommandIds.add(value['uuid']);
|
|
445
446
|
});
|
|
446
447
|
forEachJsonlRecord(truncated ? window.pageContent : content, (value) => {
|
|
448
|
+
if (runner === 'codex' && value['type'] === 'event_msg' && isRecord(value['payload'])) {
|
|
449
|
+
const payload = value['payload'];
|
|
450
|
+
if (payload['type'] === 'task_started')
|
|
451
|
+
codexTurnId = typeof payload['turn_id'] === 'string' ? payload['turn_id'] : undefined;
|
|
452
|
+
else if (payload['type'] === 'task_complete')
|
|
453
|
+
codexTurnId = undefined;
|
|
454
|
+
}
|
|
447
455
|
const recordId = typeof value['uuid'] === 'string' ? value['uuid'] : undefined;
|
|
448
456
|
const parentId = typeof value['parentUuid'] === 'string' ? value['parentUuid'] : undefined;
|
|
449
457
|
if (value['isMeta'] === true && recordId !== undefined)
|
|
@@ -469,7 +477,16 @@ async function readTranscript(path, runner, byteLimit, page, targetLines = Numbe
|
|
|
469
477
|
if (generatedImage !== undefined)
|
|
470
478
|
items.push(generatedImage);
|
|
471
479
|
for (const parsedItem of transcriptItems(runner, value)) {
|
|
472
|
-
const item =
|
|
480
|
+
const item = {
|
|
481
|
+
...parsedItem,
|
|
482
|
+
...(recordId === undefined ? {} : { nativeId: recordId }),
|
|
483
|
+
...(runner === 'codex' &&
|
|
484
|
+
parsedItem.kind === 'message' &&
|
|
485
|
+
parsedItem.role === 'USER' &&
|
|
486
|
+
codexTurnId !== undefined
|
|
487
|
+
? { nativeTurnId: codexTurnId }
|
|
488
|
+
: {})
|
|
489
|
+
};
|
|
473
490
|
if (item.kind === 'tool_result') {
|
|
474
491
|
const toolIndex = toolIndexes.get(item.toolUseId);
|
|
475
492
|
if (toolIndex !== undefined)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { basename, dirname, join, resolve } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
/** Environment for processes started by this Node, not for the Node service itself. */
|
|
5
|
+
export function nodeExecutionEnvironment(provided = {}, inherited = process.env, options = {}) {
|
|
6
|
+
const environment = Object.fromEntries(Object.entries(inherited).filter((entry) => typeof entry[1] === 'string'));
|
|
7
|
+
Object.assign(environment, provided);
|
|
8
|
+
environment['MAR_NODE_CONTEXT'] = '1';
|
|
9
|
+
const platform = options.platform ?? process.platform;
|
|
10
|
+
const packageRoot = options.packageRoot ?? resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
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)
|
|
26
|
+
return environment;
|
|
27
|
+
const separator = platform === 'win32' ? ';' : ':';
|
|
28
|
+
const existingKeys = Object.keys(environment).filter((key) => key.toLowerCase() === 'path');
|
|
29
|
+
const key = platform === 'win32'
|
|
30
|
+
? (Object.keys(provided).find((name) => name.toLowerCase() === 'path') ??
|
|
31
|
+
existingKeys[0] ??
|
|
32
|
+
'Path')
|
|
33
|
+
: 'PATH';
|
|
34
|
+
const current = environment[key] ?? environment[existingKeys[0] ?? key] ?? '';
|
|
35
|
+
if (platform === 'win32')
|
|
36
|
+
for (const other of existingKeys)
|
|
37
|
+
if (other !== key)
|
|
38
|
+
delete environment[other];
|
|
39
|
+
const samePath = (entry) => platform === 'win32'
|
|
40
|
+
? entry.replaceAll('/', '\\').toLowerCase() === cliBin.replaceAll('/', '\\').toLowerCase()
|
|
41
|
+
: entry === cliBin;
|
|
42
|
+
environment[key] = [
|
|
43
|
+
cliBin,
|
|
44
|
+
...current.split(separator).filter((entry) => entry && !samePath(entry))
|
|
45
|
+
].join(separator);
|
|
46
|
+
return environment;
|
|
47
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { mcpRuntimeDescriptorSchema } from '@myagentroam/protocol';
|
|
2
|
+
import { nodeExecutionEnvironment } from '../node-execution-environment.js';
|
|
2
3
|
/**
|
|
3
4
|
* Stable lifecycle boundary for every Node Runner implementation.
|
|
4
5
|
*
|
|
@@ -175,6 +176,7 @@ function resolvedMcpServers(raw, secrets) {
|
|
|
175
176
|
: { MAR_MGR_USER_TOKEN: secrets['MAR_MGR_USER_TOKEN'] }),
|
|
176
177
|
MAR_MGR_CONTEXT_REQUIRED: '1'
|
|
177
178
|
};
|
|
179
|
+
const mcpEnvironment = (provided) => nodeExecutionEnvironment({ ...provided, ...marmgrEnvironment }, { PATH: process.env['PATH'], Path: process.env['Path'] });
|
|
178
180
|
const result = [];
|
|
179
181
|
for (const value of raw) {
|
|
180
182
|
if (value === null || typeof value !== 'object' || Array.isArray(value))
|
|
@@ -197,24 +199,21 @@ function resolvedMcpServers(raw, secrets) {
|
|
|
197
199
|
transport: 'STDIO',
|
|
198
200
|
command: record['command'],
|
|
199
201
|
args: record['args'],
|
|
200
|
-
environment: record['__marGoal'] === true
|
|
202
|
+
environment: mcpEnvironment(record['__marGoal'] === true
|
|
201
203
|
? {
|
|
202
204
|
MAR_GOAL_MCP_ENDPOINT: record['endpoint'],
|
|
203
|
-
MAR_GOAL_MCP_TOKEN: secrets[record['secretName']]
|
|
204
|
-
...marmgrEnvironment
|
|
205
|
+
MAR_GOAL_MCP_TOKEN: secrets[record['secretName']]
|
|
205
206
|
}
|
|
206
207
|
: record['__marScheduled'] === true
|
|
207
208
|
? {
|
|
208
209
|
MAR_SCHEDULE_MCP_ENDPOINT: record['endpoint'],
|
|
209
210
|
MAR_SCHEDULE_MCP_TOKEN: secrets[record['secretName']],
|
|
210
|
-
MAR_SCHEDULE_MCP_READ_ONLY: record['readOnly'] === true ? '1' : '0'
|
|
211
|
-
...marmgrEnvironment
|
|
211
|
+
MAR_SCHEDULE_MCP_READ_ONLY: record['readOnly'] === true ? '1' : '0'
|
|
212
212
|
}
|
|
213
213
|
: {
|
|
214
214
|
MAR_MAIL_MCP_ENDPOINT: record['endpoint'],
|
|
215
|
-
MAR_MAIL_MCP_TOKEN: secrets[record['secretName']]
|
|
216
|
-
|
|
217
|
-
}
|
|
215
|
+
MAR_MAIL_MCP_TOKEN: secrets[record['secretName']]
|
|
216
|
+
})
|
|
218
217
|
});
|
|
219
218
|
continue;
|
|
220
219
|
}
|
|
@@ -251,11 +250,10 @@ function resolvedMcpServers(raw, secrets) {
|
|
|
251
250
|
? configuration['arguments'].filter((item) => typeof item === 'string')
|
|
252
251
|
: [])
|
|
253
252
|
],
|
|
254
|
-
environment: {
|
|
253
|
+
environment: mcpEnvironment({
|
|
255
254
|
...runtimeEnvironment,
|
|
256
|
-
...configuredEnvironment
|
|
257
|
-
|
|
258
|
-
}
|
|
255
|
+
...configuredEnvironment
|
|
256
|
+
})
|
|
259
257
|
});
|
|
260
258
|
}
|
|
261
259
|
else {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createEnvelope } from '@myagentroam/protocol';
|
|
2
2
|
import { ClaudeChannelGate } from '../../claude-channel.js';
|
|
3
|
+
import { nodeExecutionEnvironment } from '../../node-execution-environment.js';
|
|
3
4
|
import { boundedConversationItemId, claudePlanText, compactRunnerText, extractClaudeText, extractClaudeThinkingSummaries, extractClaudeToolResults, extractClaudeToolUses } from '../codex/conversation-parser.js';
|
|
4
5
|
import { normalizeClaudeTool } from './tool-normalizer.js';
|
|
5
6
|
import { claudeContentBlocks } from '../../util/node-operation-parsers.js';
|
|
@@ -170,7 +171,10 @@ export class ClaudeManagedRunController {
|
|
|
170
171
|
...(typeof payload.model === 'string' ? { model: payload.model } : {}),
|
|
171
172
|
...(typeof payload.effort === 'string' ? { effort: payload.effort } : {}),
|
|
172
173
|
...(typeof payload.access === 'string' ? { access: payload.access } : {}),
|
|
173
|
-
environment: {
|
|
174
|
+
environment: nodeExecutionEnvironment({
|
|
175
|
+
...secretEnvironment,
|
|
176
|
+
MAR_MGR_CONTEXT_REQUIRED: '1'
|
|
177
|
+
}, { PATH: process.env['PATH'], Path: process.env['Path'] }),
|
|
174
178
|
...(typeof payload.personalInstructions === 'string'
|
|
175
179
|
? { personalInstructions: payload.personalInstructions }
|
|
176
180
|
: {}),
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { nodeExecutionEnvironment } from '../../node-execution-environment.js';
|
|
1
2
|
import { extractId } from '../../util/runner-native-session-parsers.js';
|
|
2
3
|
import { parseSecretEnvironment } from '../../util/secret-environment.js';
|
|
3
4
|
import { safeErrorCode } from '../../util/safe-error.js';
|
|
@@ -65,7 +66,7 @@ export class CodexManagedRunController {
|
|
|
65
66
|
throw new Error('RUNNER_COMMAND_UNAVAILABLE');
|
|
66
67
|
if (command.id === 'compact') {
|
|
67
68
|
const runId = crypto.randomUUID();
|
|
68
|
-
this.runner.prepareEnvironment(context.secretEnvironment, runId);
|
|
69
|
+
this.runner.prepareEnvironment(nodeExecutionEnvironment({ ...context.secretEnvironment, MAR_MGR_CONTEXT_REQUIRED: '1' }, { PATH: process.env['PATH'], Path: process.env['Path'] }), runId);
|
|
69
70
|
try {
|
|
70
71
|
await this.runner.start(runId);
|
|
71
72
|
return await this.compact(session, threadId, runId, context.initiatedByUserId);
|
|
@@ -385,7 +386,7 @@ export class CodexManagedRunController {
|
|
|
385
386
|
return;
|
|
386
387
|
}
|
|
387
388
|
try {
|
|
388
|
-
this.runner.prepareEnvironment({ ...secretEnvironment, MAR_MGR_CONTEXT_REQUIRED: '1' }, runId);
|
|
389
|
+
this.runner.prepareEnvironment(nodeExecutionEnvironment({ ...secretEnvironment, MAR_MGR_CONTEXT_REQUIRED: '1' }, { PATH: process.env['PATH'], Path: process.env['Path'] }), runId);
|
|
389
390
|
}
|
|
390
391
|
catch (error) {
|
|
391
392
|
this.host.emit(runId, 'run.rejected', { code: safeErrorCode(error, 'SECRET_ENVIRONMENT_INVALID') }, 'FAILED');
|
|
@@ -2,6 +2,7 @@ import { computerUseHostSkill } from './computer-use-skill.js';
|
|
|
2
2
|
import { createHash, randomUUID } from 'node:crypto';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { access, constants } from 'node:fs/promises';
|
|
5
|
+
import { nodeExecutionEnvironment } from '../../node-execution-environment.js';
|
|
5
6
|
import { BUNDLED_RIPGREP_VERSION, bundledRipgrepPath, prependBundledRipgrepToPath } from '../../ripgrep-binary.js';
|
|
6
7
|
import { LocalAgentHost, createMarAgent } from '@myagentroam/agent';
|
|
7
8
|
import { marAgentReasoningEffortSchema, marAgentRuntimeModelConfigurationSchema } from '@myagentroam/protocol';
|
|
@@ -686,7 +687,10 @@ export function allowsUntrustedWebFetchTls(environment) {
|
|
|
686
687
|
return environment['MAR_AGENT_WEB_FETCH_ALLOW_UNTRUSTED_TLS'] !== '0';
|
|
687
688
|
}
|
|
688
689
|
async function resolveMarAgentHostRuntimeConfiguration(secretEnvironment = {}) {
|
|
689
|
-
const environment = {
|
|
690
|
+
const environment = nodeExecutionEnvironment({
|
|
691
|
+
...secretEnvironment,
|
|
692
|
+
MAR_MGR_CONTEXT_REQUIRED: '1'
|
|
693
|
+
});
|
|
690
694
|
const ripgrep = await availableBundledRipgrep();
|
|
691
695
|
return {
|
|
692
696
|
environment: ripgrep === undefined ? environment : prependBundledRipgrepToPath(environment, ripgrep),
|
|
@@ -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;
|
|
@@ -73,6 +74,7 @@ export class MarAgentRuntimeModelFactory {
|
|
|
73
74
|
...(configuration.responsesEncoding === undefined
|
|
74
75
|
? {}
|
|
75
76
|
: { responsesEncoding: configuration.responsesEncoding }),
|
|
77
|
+
responsesToolNamespace: configuration.responsesToolNamespace,
|
|
76
78
|
responsesPreviousResponseId: configuration.responsesPreviousResponseId,
|
|
77
79
|
enabled: configuration.enabled
|
|
78
80
|
};
|
|
@@ -229,12 +231,12 @@ class ServerRuntimeCredentialProvider {
|
|
|
229
231
|
if (this.state.pending !== pending)
|
|
230
232
|
return;
|
|
231
233
|
delete this.state.pending;
|
|
232
|
-
scheduleRefresh(this.state, this.now);
|
|
234
|
+
scheduleRefresh(this.state, this.now, CREDENTIAL_REFRESH_RETRY_MS);
|
|
233
235
|
}, () => {
|
|
234
236
|
if (this.state.pending !== pending)
|
|
235
237
|
return;
|
|
236
238
|
delete this.state.pending;
|
|
237
|
-
scheduleRefresh(this.state, this.now,
|
|
239
|
+
scheduleRefresh(this.state, this.now, CREDENTIAL_REFRESH_RETRY_MS);
|
|
238
240
|
});
|
|
239
241
|
return pending;
|
|
240
242
|
}
|
|
@@ -296,20 +298,23 @@ function sameRuntimeCredentialTicket(first, second) {
|
|
|
296
298
|
first.allowedModelIds.length === second.allowedModelIds.length &&
|
|
297
299
|
first.allowedModelIds.every((modelId, index) => modelId === second.allowedModelIds[index]));
|
|
298
300
|
}
|
|
299
|
-
function scheduleRefresh(state, now,
|
|
301
|
+
function scheduleRefresh(state, now, minimumDelayMs = 0) {
|
|
300
302
|
clearRefreshTimer(state);
|
|
301
303
|
if (state.released || state.pending !== undefined || state.providers.size === 0)
|
|
302
304
|
return;
|
|
305
|
+
const timestamp = now();
|
|
306
|
+
if (state.ticket.expiresAt <= timestamp)
|
|
307
|
+
return;
|
|
303
308
|
const expiresAt = Math.min(state.ticket.expiresAt, state.credential?.expiresAt ?? Number.POSITIVE_INFINITY);
|
|
304
|
-
const delay = expiresAt -
|
|
305
|
-
if (
|
|
309
|
+
const delay = Math.max(minimumDelayMs, expiresAt - timestamp - CREDENTIAL_REFRESH_WINDOW_MS, 0);
|
|
310
|
+
if (timestamp + delay > state.ticket.expiresAt)
|
|
306
311
|
return;
|
|
307
312
|
const timer = setTimeout(() => {
|
|
308
313
|
if (state.refreshTimer === timer)
|
|
309
314
|
delete state.refreshTimer;
|
|
310
315
|
const provider = state.providers.values().next().value;
|
|
311
316
|
provider?.refreshAutomatically();
|
|
312
|
-
},
|
|
317
|
+
}, delay);
|
|
313
318
|
timer.unref();
|
|
314
319
|
state.refreshTimer = timer;
|
|
315
320
|
}
|
|
@@ -221,24 +221,36 @@ export class MarAgentRunner extends AbstractRunner {
|
|
|
221
221
|
let catalog;
|
|
222
222
|
try {
|
|
223
223
|
catalog = await openMarAgentSessionCatalog({ homeDirectory: this.homeDirectory });
|
|
224
|
-
const
|
|
224
|
+
const activeCatalog = catalog;
|
|
225
|
+
const listed = await activeCatalog.list({ limit: 200 });
|
|
225
226
|
const workspaces = workspace ? [workspace] : context.workspaces;
|
|
226
|
-
return listed.sessions.
|
|
227
|
+
return (await Promise.all(listed.sessions.map(async (session) => {
|
|
227
228
|
const target = workspaces.find((candidate) => resolve(candidate.path) === resolve(session.workspace));
|
|
228
229
|
if (!target)
|
|
229
230
|
return [];
|
|
231
|
+
const executionConfiguration = session.modelId === undefined
|
|
232
|
+
? await activeCatalog.executionConfiguration(session.id)
|
|
233
|
+
: session;
|
|
230
234
|
return [
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
235
|
+
{
|
|
236
|
+
...context.createProjection({
|
|
237
|
+
workspace: target,
|
|
238
|
+
runner: this.name,
|
|
239
|
+
externalSessionId: session.id,
|
|
240
|
+
cwd: session.workspace,
|
|
241
|
+
title: session.title,
|
|
242
|
+
titleOrigin: 'NATIVE',
|
|
243
|
+
lastActivityAt: Date.parse(session.lastActivityAt)
|
|
244
|
+
}),
|
|
245
|
+
...(executionConfiguration?.modelId === undefined
|
|
246
|
+
? {}
|
|
247
|
+
: { model: executionConfiguration.modelId }),
|
|
248
|
+
...(executionConfiguration?.reasoningEffort === undefined
|
|
249
|
+
? {}
|
|
250
|
+
: { effort: executionConfiguration.reasoningEffort })
|
|
251
|
+
}
|
|
240
252
|
];
|
|
241
|
-
});
|
|
253
|
+
}))).flat();
|
|
242
254
|
}
|
|
243
255
|
catch {
|
|
244
256
|
return [];
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { nodeExecutionEnvironment } from '../../node-execution-environment.js';
|
|
1
2
|
import { parseSecretEnvironment } from '../../util/secret-environment.js';
|
|
2
3
|
import { runnerErrorProjection } from '../../util/runner-error.js';
|
|
3
4
|
import { boundedConversationItemId, compactRunnerText } from '../codex/conversation-parser.js';
|
|
@@ -49,7 +50,7 @@ export class OpenCodeManagedRunController {
|
|
|
49
50
|
this.host.hasActiveSessionLease(session.id))
|
|
50
51
|
throw new Error('RUNNER_COMMAND_UNAVAILABLE');
|
|
51
52
|
const runId = crypto.randomUUID();
|
|
52
|
-
const client = this.runner.managedClient(environment);
|
|
53
|
+
const client = this.runner.managedClient(nodeExecutionEnvironment({ ...environment, MAR_MGR_CONTEXT_REQUIRED: '1' }, { PATH: process.env['PATH'], Path: process.env['Path'] }));
|
|
53
54
|
this.host.adopt({
|
|
54
55
|
runId,
|
|
55
56
|
sessionId: session.id,
|
|
@@ -108,10 +109,10 @@ export class OpenCodeManagedRunController {
|
|
|
108
109
|
let mcp;
|
|
109
110
|
let attachments;
|
|
110
111
|
try {
|
|
111
|
-
environment = {
|
|
112
|
+
environment = nodeExecutionEnvironment({
|
|
112
113
|
...parseSecretEnvironment(payload.secretEnvironment),
|
|
113
114
|
MAR_MGR_CONTEXT_REQUIRED: '1'
|
|
114
|
-
};
|
|
115
|
+
}, { PATH: process.env['PATH'], Path: process.env['Path'] });
|
|
115
116
|
mcpSecretEnvironment = parseSecretEnvironment(payload.mcpSecretEnvironment);
|
|
116
117
|
mcp = this.runner.mcpConfiguration(payload.mcpInstallations, {
|
|
117
118
|
...environment,
|
|
@@ -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
|
+
}
|
|
@@ -369,20 +369,22 @@ export class ConversationHistoryService {
|
|
|
369
369
|
}
|
|
370
370
|
async readManagedRuntime(session, input, cursor, snapshotSequence, readAt) {
|
|
371
371
|
const page = await this.readManagedActive(session, input, cursor, snapshotSequence, readAt);
|
|
372
|
+
const codexActive = session.runner === 'codex' && this.options.hasManagedActiveRun(session);
|
|
372
373
|
if (input.cursor !== undefined ||
|
|
373
374
|
session.externalSessionId === null ||
|
|
374
|
-
|
|
375
|
-
|
|
375
|
+
(!codexActive &&
|
|
376
|
+
(this.options.hasManagedActiveRun(session) || !this.hasManagedRuntimeHistory(session))) ||
|
|
376
377
|
!page.turns.some((turn) => turn.rewind === undefined &&
|
|
377
378
|
terminalConversationTurn(turn.status) &&
|
|
378
379
|
conversationClientMessageId(turn) !== null))
|
|
379
380
|
return page;
|
|
380
381
|
// A completed managed Turn keeps its live items and Run identity until Runtime cleanup.
|
|
381
|
-
// Only
|
|
382
|
-
const
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
382
|
+
// Only its matched native Turn may supply a Rewind boundary.
|
|
383
|
+
const historyInput = { limit: Math.min(500, Math.max(30, page.turns.length + 1)) };
|
|
384
|
+
const authoritative = codexActive
|
|
385
|
+
? (await this.readRunnerHistory(session, historyInput).catch(() => undefined))?.page
|
|
386
|
+
: await this.readOfficial(session, historyInput).catch(() => undefined);
|
|
387
|
+
if (authoritative === undefined)
|
|
386
388
|
return page;
|
|
387
389
|
return {
|
|
388
390
|
...page,
|
|
@@ -392,7 +394,7 @@ export class ConversationHistoryService {
|
|
|
392
394
|
const messageId = conversationClientMessageId(turn);
|
|
393
395
|
if (messageId === null)
|
|
394
396
|
return turn;
|
|
395
|
-
const native =
|
|
397
|
+
const native = authoritative.turns.find((candidate) => candidate.rewind !== undefined &&
|
|
396
398
|
candidate.userItemId !== null &&
|
|
397
399
|
conversationClientMessageId(candidate) === messageId);
|
|
398
400
|
return native?.rewind === undefined ? turn : { ...turn, rewind: native.rewind };
|
package/dist/terminal.js
CHANGED
|
@@ -5,6 +5,7 @@ import { homedir } from 'node:os';
|
|
|
5
5
|
import { join } from 'node:path';
|
|
6
6
|
import { spawn } from 'node-pty';
|
|
7
7
|
import { terminalDataFrameSchema } from '@myagentroam/protocol';
|
|
8
|
+
import { nodeExecutionEnvironment } from './node-execution-environment.js';
|
|
8
9
|
import { terminateProcessTree } from './process-tree.js';
|
|
9
10
|
const MAX_FRAME_CHARACTERS = 65_536;
|
|
10
11
|
const TERMINAL_COMMAND_MAX_BYTES = 64 * 1024;
|
|
@@ -138,7 +139,7 @@ export class TerminalManager {
|
|
|
138
139
|
cols,
|
|
139
140
|
rows,
|
|
140
141
|
cwd: typeof input.cwd === 'string' ? input.cwd : this.cwd,
|
|
141
|
-
env:
|
|
142
|
+
env: nodeExecutionEnvironment(input.environment, process.env, { platform: this.platform })
|
|
142
143
|
};
|
|
143
144
|
const commandArgs = command === undefined
|
|
144
145
|
? undefined
|
|
@@ -182,6 +182,11 @@ export function nativeConversationPage(session, history, input, runtimeTurns = [
|
|
|
182
182
|
const startedAt = items[0]?.startedAt ?? null;
|
|
183
183
|
const completedAt = items.at(-1)?.completedAt ?? startedAt;
|
|
184
184
|
const after = lastNativeItemId(entries);
|
|
185
|
+
const nativeIndex = start + relativeIndex;
|
|
186
|
+
const turnBoundaryAfter = nativeUser?.kind === 'message' ? nativeUser.nativeTurnId : undefined;
|
|
187
|
+
const previousNativeUser = nativeTurns[nativeIndex - 1]?.find(({ entry }) => entry.kind === 'message' && entry.role === 'USER')?.entry;
|
|
188
|
+
const turnBoundaryBefore = previousNativeUser?.kind === 'message' ? previousNativeUser.nativeTurnId : undefined;
|
|
189
|
+
const firstNativeTurn = nativeIndex === 0 && history.truncated !== true;
|
|
185
190
|
return {
|
|
186
191
|
id: turnId,
|
|
187
192
|
sessionId: session.id,
|
|
@@ -199,6 +204,10 @@ export function nativeConversationPage(session, history, input, runtimeTurns = [
|
|
|
199
204
|
after
|
|
200
205
|
}
|
|
201
206
|
}),
|
|
207
|
+
...(turnBoundaryAfter === undefined ||
|
|
208
|
+
(!firstNativeTurn && turnBoundaryBefore === undefined)
|
|
209
|
+
? {}
|
|
210
|
+
: { rewind: { before: turnBoundaryBefore ?? null, after: turnBoundaryAfter } }),
|
|
202
211
|
items,
|
|
203
212
|
startedAt,
|
|
204
213
|
completedAt
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@myagentroam/node",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.134",
|
|
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/agent": "0.9.
|
|
39
|
-
"@myagentroam/
|
|
40
|
-
"@myagentroam/
|
|
37
|
+
"@myagentroam/marmgr": "0.9.117",
|
|
38
|
+
"@myagentroam/agent": "0.9.134",
|
|
39
|
+
"@myagentroam/code-navigation": "0.9.129",
|
|
40
|
+
"@myagentroam/protocol": "0.9.134"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@types/ws": "^8.18.1"
|