@myagentroam/node 0.1.8 → 0.9.0
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 +6 -3
- package/dist/claude-agent-sdk.d.ts +1 -0
- package/dist/claude-agent-sdk.js +8 -1
- package/dist/codex-app-server.d.ts +6 -0
- package/dist/codex-app-server.js +6 -0
- package/dist/connector/node-connector-options.d.ts +1 -0
- package/dist/connector.js +5 -2
- package/dist/database.d.ts +23 -0
- package/dist/database.js +84 -2
- package/dist/migrations/v004.d.ts +4 -0
- package/dist/migrations/v004.js +35 -0
- package/dist/opencode-server.d.ts +1 -0
- package/dist/opencode-server.js +12 -0
- package/dist/runner/abstract-runner.d.ts +12 -2
- package/dist/runner/abstract-runner.js +76 -2
- package/dist/runner/claude/managed-run-controller.js +2 -1
- package/dist/runner/claude-code-runner.d.ts +1 -0
- package/dist/runner/claude-code-runner.js +14 -0
- package/dist/runner/codex/managed-run-controller.js +5 -3
- package/dist/runner/codex-runner.d.ts +16 -2
- package/dist/runner/codex-runner.js +215 -4
- package/dist/runner/opencode/managed-run-controller.js +13 -3
- package/dist/runner/opencode-runner.d.ts +2 -1
- package/dist/runner/opencode-runner.js +23 -3
- package/dist/runner/runner-registry.d.ts +1 -1
- package/dist/runner/runner-registry.js +2 -2
- package/dist/runner-profiles.js +5 -25
- package/dist/runtime-command-detector.d.ts +3 -0
- package/dist/runtime-command-detector.js +78 -0
- package/dist/service/mcp-installation-verifier.d.ts +9 -0
- package/dist/service/mcp-installation-verifier.js +87 -0
- package/dist/service/mcp-node-operation-service.d.ts +11 -0
- package/dist/service/mcp-node-operation-service.js +90 -0
- package/dist/service/mcp-package-installer.d.ts +8 -0
- package/dist/service/mcp-package-installer.js +136 -0
- package/dist/service/runner-service.d.ts +2 -0
- package/dist/service/runner-service.js +5 -2
- package/dist/service/session-lifecycle-service.js +7 -4
- package/dist/service/workbench-manifest-service.d.ts +2 -0
- package/dist/service/workspace-queue-workbench-service.d.ts +2 -0
- package/dist/service/workspace-queue-workbench-service.js +2 -1
- package/package.json +2 -2
|
@@ -9,24 +9,147 @@ import { readCodexNativeContextUsage } from '../native-session-history.js';
|
|
|
9
9
|
import { codexActiveTurnId, codexOfficialConversationPage, codexThreadCwd, codexThreadActivity, deduplicateDirectSessions, externalResumeFailureDetail, extractCodexThreads, isWithinWorkspace, latestNativeActivity } from './codex/conversation-parser.js';
|
|
10
10
|
import { codexSessionControl } from '../codex-app-server.js';
|
|
11
11
|
import { extractId } from '../util/runner-native-session-parsers.js';
|
|
12
|
+
import { nodeLog } from '../operational.js';
|
|
13
|
+
const CODEX_MODEL_CACHE_MS = 10 * 60 * 1_000;
|
|
14
|
+
const CODEX_MODEL_FAILURE_RETRY_MS = 30 * 1_000;
|
|
15
|
+
const CODEX_MODEL_PAGE_SIZE = 100;
|
|
12
16
|
export class CodexRunner extends AbstractRunner {
|
|
13
17
|
client;
|
|
18
|
+
profileClientFactory;
|
|
14
19
|
name = 'codex';
|
|
15
20
|
discoveryScope = 'ALL_WORKSPACES';
|
|
16
21
|
execution = new CodexExecutionState();
|
|
17
22
|
fastEnabled = false;
|
|
18
23
|
environmentFingerprint;
|
|
19
24
|
environmentRuns = new Set();
|
|
20
|
-
|
|
25
|
+
profileCache = new Map();
|
|
26
|
+
profileRefreshes = new Map();
|
|
27
|
+
profileClients = new Set();
|
|
28
|
+
profileKeyByEnvironment = new Map();
|
|
29
|
+
latestProfileCache;
|
|
30
|
+
selectedProfileKey;
|
|
31
|
+
constructor(client = new CodexAppServerClient(), profileClientFactory = () => new CodexAppServerClient()) {
|
|
21
32
|
super();
|
|
22
33
|
this.client = client;
|
|
34
|
+
this.profileClientFactory = profileClientFactory;
|
|
23
35
|
}
|
|
24
36
|
profile(capabilities) {
|
|
25
|
-
|
|
37
|
+
const base = declaredRunnerProfiles({
|
|
26
38
|
codexAvailable: capabilities.codex.available,
|
|
27
39
|
claudeCodeAvailable: capabilities.claudeCode.available,
|
|
28
40
|
openCodeAvailable: capabilities.openCode?.available === true
|
|
29
41
|
}).find((profile) => profile.runner === this.name);
|
|
42
|
+
return { ...base, models: this.latestProfileCache?.profile.models ?? [] };
|
|
43
|
+
}
|
|
44
|
+
async refreshProfile(capabilities, workspace, environment = {}) {
|
|
45
|
+
void workspace;
|
|
46
|
+
const base = this.profile(capabilities);
|
|
47
|
+
if (!capabilities.codex.available)
|
|
48
|
+
return { ...base, models: [] };
|
|
49
|
+
const key = codexEnvironmentFingerprint(environment, capabilities.codex.version);
|
|
50
|
+
this.profileKeyByEnvironment.set(codexSecretEnvironmentFingerprint(environment), key);
|
|
51
|
+
this.selectedProfileKey = key;
|
|
52
|
+
const now = Date.now();
|
|
53
|
+
const cached = this.profileCache.get(key);
|
|
54
|
+
if (cached?.successful === true && now < cached.fetchedAt + CODEX_MODEL_CACHE_MS)
|
|
55
|
+
return this.selectCachedProfile(key, cached);
|
|
56
|
+
if (cached?.successful === true) {
|
|
57
|
+
if (now >= cached.retryAt)
|
|
58
|
+
void this.refreshCodexProfile(key, base, environment);
|
|
59
|
+
return this.selectCachedProfile(key, cached);
|
|
60
|
+
}
|
|
61
|
+
if (cached !== undefined && now < cached.retryAt)
|
|
62
|
+
return cached.profile;
|
|
63
|
+
return this.refreshCodexProfile(key, base, environment);
|
|
64
|
+
}
|
|
65
|
+
refreshCodexProfile(key, base, environment) {
|
|
66
|
+
const existing = this.profileRefreshes.get(key);
|
|
67
|
+
if (existing !== undefined)
|
|
68
|
+
return existing;
|
|
69
|
+
const refresh = this.fetchCodexProfile(base, environment)
|
|
70
|
+
.then((value) => {
|
|
71
|
+
const cache = {
|
|
72
|
+
...value,
|
|
73
|
+
fetchedAt: Date.now(),
|
|
74
|
+
successful: true,
|
|
75
|
+
retryAt: 0
|
|
76
|
+
};
|
|
77
|
+
this.profileCache.set(key, cache);
|
|
78
|
+
if (this.selectedProfileKey === key)
|
|
79
|
+
this.latestProfileCache = cache;
|
|
80
|
+
return cache.profile;
|
|
81
|
+
})
|
|
82
|
+
.catch((error) => {
|
|
83
|
+
nodeLog('runner.codex.models.refresh-failed', {
|
|
84
|
+
error: error instanceof Error ? error.message : String(error)
|
|
85
|
+
});
|
|
86
|
+
const cached = this.profileCache.get(key);
|
|
87
|
+
if (cached !== undefined) {
|
|
88
|
+
cached.retryAt = Date.now() + CODEX_MODEL_FAILURE_RETRY_MS;
|
|
89
|
+
return cached.profile;
|
|
90
|
+
}
|
|
91
|
+
this.profileCache.set(key, {
|
|
92
|
+
profile: { ...base, models: [] },
|
|
93
|
+
defaultConfiguration: emptyCodexConfiguration(),
|
|
94
|
+
fetchedAt: 0,
|
|
95
|
+
successful: false,
|
|
96
|
+
retryAt: Date.now() + CODEX_MODEL_FAILURE_RETRY_MS
|
|
97
|
+
});
|
|
98
|
+
return { ...base, models: [] };
|
|
99
|
+
})
|
|
100
|
+
.finally(() => this.profileRefreshes.delete(key));
|
|
101
|
+
this.profileRefreshes.set(key, refresh);
|
|
102
|
+
return refresh;
|
|
103
|
+
}
|
|
104
|
+
selectCachedProfile(key, cached) {
|
|
105
|
+
if (cached.successful && this.selectedProfileKey === key)
|
|
106
|
+
this.latestProfileCache = cached;
|
|
107
|
+
return cached.profile;
|
|
108
|
+
}
|
|
109
|
+
async fetchCodexProfile(base, environment) {
|
|
110
|
+
const client = this.profileClientFactory();
|
|
111
|
+
this.profileClients.add(client);
|
|
112
|
+
if (Object.keys(environment).length > 0)
|
|
113
|
+
client.configureEnvironment(environment);
|
|
114
|
+
try {
|
|
115
|
+
await client.start();
|
|
116
|
+
const models = [];
|
|
117
|
+
let cursor = null;
|
|
118
|
+
const cursors = new Set();
|
|
119
|
+
do {
|
|
120
|
+
const page = codexModelPage(await client.listModels({ cursor, limit: CODEX_MODEL_PAGE_SIZE, includeHidden: false }));
|
|
121
|
+
models.push(...page.models);
|
|
122
|
+
cursor = page.nextCursor;
|
|
123
|
+
if (cursor !== null && cursors.has(cursor))
|
|
124
|
+
throw new Error('CODEX_MODELS_CURSOR_REPEATED');
|
|
125
|
+
if (cursor !== null)
|
|
126
|
+
cursors.add(cursor);
|
|
127
|
+
} while (cursor !== null);
|
|
128
|
+
const uniqueModels = models.filter((model, index) => models.findIndex((candidate) => candidate.model === model.model) === index);
|
|
129
|
+
const profileModels = uniqueModels.map((model) => ({
|
|
130
|
+
id: model.model,
|
|
131
|
+
label: model.displayName,
|
|
132
|
+
supportedEfforts: [...model.supportedEfforts],
|
|
133
|
+
isDefault: model.isDefault,
|
|
134
|
+
defaultEffort: model.defaultEffort
|
|
135
|
+
}));
|
|
136
|
+
const selected = uniqueModels.find((model) => model.isDefault) ?? uniqueModels[0];
|
|
137
|
+
return {
|
|
138
|
+
profile: { ...base, models: profileModels },
|
|
139
|
+
defaultConfiguration: selected === undefined
|
|
140
|
+
? emptyCodexConfiguration()
|
|
141
|
+
: {
|
|
142
|
+
runner: this.name,
|
|
143
|
+
model: selected.model,
|
|
144
|
+
effort: selected.defaultEffort,
|
|
145
|
+
access: 'on-request'
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
finally {
|
|
150
|
+
if (this.profileClients.delete(client))
|
|
151
|
+
client.stop();
|
|
152
|
+
}
|
|
30
153
|
}
|
|
31
154
|
available(capabilities) {
|
|
32
155
|
return capabilities.codex.available;
|
|
@@ -44,6 +167,19 @@ export class CodexRunner extends AbstractRunner {
|
|
|
44
167
|
serviceTier() {
|
|
45
168
|
return this.fastEnabled ? 'fast' : undefined;
|
|
46
169
|
}
|
|
170
|
+
mcpConfiguration(raw, secrets) {
|
|
171
|
+
const servers = super.mcpConfiguration(raw, secrets);
|
|
172
|
+
return Object.fromEntries(servers.map((server) => [
|
|
173
|
+
server.name,
|
|
174
|
+
server.transport === 'STDIO'
|
|
175
|
+
? {
|
|
176
|
+
command: server.command,
|
|
177
|
+
args: [...(server.args ?? [])],
|
|
178
|
+
env: { ...(server.environment ?? {}) }
|
|
179
|
+
}
|
|
180
|
+
: { url: server.url, http_headers: { ...(server.headers ?? {}) } }
|
|
181
|
+
]));
|
|
182
|
+
}
|
|
47
183
|
usesRunnerTitleForRename() {
|
|
48
184
|
return true;
|
|
49
185
|
}
|
|
@@ -131,8 +267,23 @@ export class CodexRunner extends AbstractRunner {
|
|
|
131
267
|
openCode: { available: false }
|
|
132
268
|
});
|
|
133
269
|
}
|
|
134
|
-
defaultConfiguration() {
|
|
135
|
-
return
|
|
270
|
+
defaultConfiguration(_workspace, environment) {
|
|
271
|
+
return (this.profileCacheForEnvironment(environment)?.defaultConfiguration ??
|
|
272
|
+
emptyCodexConfiguration());
|
|
273
|
+
}
|
|
274
|
+
supportsConfiguration(configuration, _workspace, environment) {
|
|
275
|
+
if (configuration.runner !== this.name)
|
|
276
|
+
return false;
|
|
277
|
+
const profile = this.profileCacheForEnvironment(environment)?.profile;
|
|
278
|
+
return (profile !== undefined &&
|
|
279
|
+
profile.models.some((model) => model.id === configuration.model && model.supportedEfforts.includes(configuration.effort)) &&
|
|
280
|
+
profile.accessOptions.some((option) => option.id === configuration.access));
|
|
281
|
+
}
|
|
282
|
+
profileCacheForEnvironment(environment) {
|
|
283
|
+
if (environment === undefined)
|
|
284
|
+
return this.latestProfileCache;
|
|
285
|
+
const key = this.profileKeyByEnvironment.get(codexSecretEnvironmentFingerprint(environment));
|
|
286
|
+
return key === undefined ? undefined : this.profileCache.get(key);
|
|
136
287
|
}
|
|
137
288
|
async resumeExternal(external, context, force = false) {
|
|
138
289
|
if (external.runner !== this.name ||
|
|
@@ -305,6 +456,9 @@ export class CodexRunner extends AbstractRunner {
|
|
|
305
456
|
this.environmentFingerprint = undefined;
|
|
306
457
|
}
|
|
307
458
|
stop() {
|
|
459
|
+
for (const profileClient of this.profileClients)
|
|
460
|
+
profileClient.stop();
|
|
461
|
+
this.profileClients.clear();
|
|
308
462
|
this.client.stop();
|
|
309
463
|
if (this.environmentFingerprint !== undefined)
|
|
310
464
|
this.client.clearEnvironment();
|
|
@@ -357,6 +511,63 @@ export class CodexRunner extends AbstractRunner {
|
|
|
357
511
|
return this.client.listCollaborationModes();
|
|
358
512
|
}
|
|
359
513
|
}
|
|
514
|
+
function emptyCodexConfiguration() {
|
|
515
|
+
return { runner: 'codex', model: '', effort: '', access: 'on-request' };
|
|
516
|
+
}
|
|
517
|
+
function codexEnvironmentFingerprint(environment, version) {
|
|
518
|
+
return createHash('sha256')
|
|
519
|
+
.update(JSON.stringify({
|
|
520
|
+
environment: Object.entries(environment).sort(([left], [right]) => left.localeCompare(right)),
|
|
521
|
+
version: version ?? null
|
|
522
|
+
}))
|
|
523
|
+
.digest('base64url');
|
|
524
|
+
}
|
|
525
|
+
function codexSecretEnvironmentFingerprint(environment) {
|
|
526
|
+
return createHash('sha256')
|
|
527
|
+
.update(JSON.stringify(Object.entries(environment).sort(([left], [right]) => left.localeCompare(right))))
|
|
528
|
+
.digest('base64url');
|
|
529
|
+
}
|
|
530
|
+
function codexModelPage(value) {
|
|
531
|
+
if (!isPlainRecord(value) || !Array.isArray(value.data))
|
|
532
|
+
throw new Error('CODEX_MODELS_INVALID');
|
|
533
|
+
return {
|
|
534
|
+
models: value.data.flatMap((entry) => {
|
|
535
|
+
if (!isPlainRecord(entry) || entry.hidden === true)
|
|
536
|
+
return [];
|
|
537
|
+
const model = typeof entry.model === 'string' ? entry.model.trim() : '';
|
|
538
|
+
const displayName = typeof entry.displayName === 'string' ? entry.displayName.trim() : '';
|
|
539
|
+
const defaultEffort = typeof entry.defaultReasoningEffort === 'string' ? entry.defaultReasoningEffort.trim() : '';
|
|
540
|
+
const supportedEfforts = Array.isArray(entry.supportedReasoningEfforts)
|
|
541
|
+
? entry.supportedReasoningEfforts.flatMap((option) => isPlainRecord(option) && typeof option.reasoningEffort === 'string'
|
|
542
|
+
? [option.reasoningEffort.trim()]
|
|
543
|
+
: [])
|
|
544
|
+
: [];
|
|
545
|
+
if (model.length === 0 ||
|
|
546
|
+
model.length > 128 ||
|
|
547
|
+
displayName.length === 0 ||
|
|
548
|
+
displayName.length > 128 ||
|
|
549
|
+
defaultEffort.length === 0 ||
|
|
550
|
+
defaultEffort.length > 64 ||
|
|
551
|
+
supportedEfforts.length > 32 ||
|
|
552
|
+
supportedEfforts.some((effort) => effort.length === 0 || effort.length > 64) ||
|
|
553
|
+
!supportedEfforts.includes(defaultEffort))
|
|
554
|
+
return [];
|
|
555
|
+
return [
|
|
556
|
+
{
|
|
557
|
+
model,
|
|
558
|
+
displayName,
|
|
559
|
+
isDefault: entry.isDefault === true,
|
|
560
|
+
defaultEffort,
|
|
561
|
+
supportedEfforts: [...new Set(supportedEfforts.filter(Boolean))]
|
|
562
|
+
}
|
|
563
|
+
];
|
|
564
|
+
}),
|
|
565
|
+
nextCursor: typeof value.nextCursor === 'string' && value.nextCursor.length > 0 ? value.nextCursor : null
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
function isPlainRecord(value) {
|
|
569
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
570
|
+
}
|
|
360
571
|
export class CodexExecutionState {
|
|
361
572
|
runs = new Map();
|
|
362
573
|
managedTurns = new Map();
|
|
@@ -32,7 +32,7 @@ export class OpenCodeManagedRunController {
|
|
|
32
32
|
detail: '后续消息将由 OpenCode 原生 Plan Agent 只读分析并制定计划。',
|
|
33
33
|
closable: true,
|
|
34
34
|
closeInput: '/plan',
|
|
35
|
-
continuation: { label: '
|
|
35
|
+
continuation: { label: '执行', prompt: '请根据上述计划开始实施。' }
|
|
36
36
|
});
|
|
37
37
|
return { command: command.id, states: this.host.commandStates(session.id) };
|
|
38
38
|
}
|
|
@@ -48,9 +48,11 @@ export class OpenCodeManagedRunController {
|
|
|
48
48
|
this.host.active.has(payload.runId))
|
|
49
49
|
return;
|
|
50
50
|
let environment;
|
|
51
|
+
let mcp;
|
|
51
52
|
let attachments;
|
|
52
53
|
try {
|
|
53
54
|
environment = parseSecretEnvironment(payload.secretEnvironment);
|
|
55
|
+
mcp = this.runner.mcpConfiguration(payload.mcpInstallations, environment);
|
|
54
56
|
attachments = this.host.parseAttachments(payload.attachments);
|
|
55
57
|
}
|
|
56
58
|
catch (error) {
|
|
@@ -70,7 +72,8 @@ export class OpenCodeManagedRunController {
|
|
|
70
72
|
environment,
|
|
71
73
|
attachments,
|
|
72
74
|
access: typeof payload.access === 'string' ? payload.access : 'default',
|
|
73
|
-
agent: payload.collaborationMode === 'plan' ? 'plan' : 'build'
|
|
75
|
+
agent: payload.collaborationMode === 'plan' ? 'plan' : 'build',
|
|
76
|
+
mcp
|
|
74
77
|
});
|
|
75
78
|
}
|
|
76
79
|
cancel(runId, intent) {
|
|
@@ -119,12 +122,16 @@ export class OpenCodeManagedRunController {
|
|
|
119
122
|
return true;
|
|
120
123
|
}
|
|
121
124
|
async run(input) {
|
|
122
|
-
|
|
125
|
+
// OpenCode 的动态 MCP 注册属于 Server 进程状态。加载 MCP 的 Run 使用独立 Server,
|
|
126
|
+
// 避免同一工作区的并行会话共享 MCP 进程、配置或浏览器上下文。
|
|
127
|
+
const client = this.runner.managedClient(input.environment, hasMcpConfiguration(input.mcp) ? input.runId : undefined);
|
|
123
128
|
this.startingClients.set(input.runId, client);
|
|
124
129
|
this.startingCwds.set(input.runId, input.cwd);
|
|
125
130
|
this.host.active.add(input.runId);
|
|
126
131
|
this.host.emit(input.runId, 'run.started', {}, 'STARTING');
|
|
127
132
|
try {
|
|
133
|
+
if (hasMcpConfiguration(input.mcp))
|
|
134
|
+
await client.configureMcp(input.cwd, input.mcp);
|
|
128
135
|
const nativeSessionId = input.externalSessionId ?? openCodeSessionId(await client.createSession(input.cwd));
|
|
129
136
|
if (nativeSessionId === undefined)
|
|
130
137
|
throw new Error('OPENCODE_SESSION_INVALID');
|
|
@@ -359,6 +366,9 @@ export class OpenCodeManagedRunController {
|
|
|
359
366
|
void this.host.captureSnapshot(runId, cwd);
|
|
360
367
|
}
|
|
361
368
|
}
|
|
369
|
+
function hasMcpConfiguration(value) {
|
|
370
|
+
return value !== null && typeof value === 'object' && Object.keys(value).length > 0;
|
|
371
|
+
}
|
|
362
372
|
function openCodePart(part, planMode = false) {
|
|
363
373
|
const id = typeof part?.id === 'string' ? part.id : undefined;
|
|
364
374
|
if (id === undefined || typeof part?.type !== 'string')
|
|
@@ -30,6 +30,7 @@ export declare class OpenCodeRunner extends AbstractRunner<'opencode'> {
|
|
|
30
30
|
protected profileForValidation(): RunnerProfile;
|
|
31
31
|
supportsConfiguration(configuration: RunnerDefaultConfiguration, workspace?: NodeWorkspace): boolean;
|
|
32
32
|
defaultConfiguration(workspace?: NodeWorkspace): RunnerDefaultConfiguration;
|
|
33
|
+
mcpConfiguration(raw: unknown, secrets: Readonly<Record<string, string>>): unknown;
|
|
33
34
|
discoverSessions(context: RunnerDiscoveryContext, workspace?: NodeWorkspace): Promise<readonly NodeAgentSession[]>;
|
|
34
35
|
resumeExternal(external: NodeAgentSession, context: ExternalResumeContext): Promise<ExternalResumeResult>;
|
|
35
36
|
readExternalActivity(session: NodeAgentSession): Promise<"UNAVAILABLE" | "EXTERNAL_ACTIVE" | "IDLE">;
|
|
@@ -55,6 +56,6 @@ export declare class OpenCodeRunner extends AbstractRunner<'opencode'> {
|
|
|
55
56
|
maxTokens: number;
|
|
56
57
|
} | undefined>;
|
|
57
58
|
stop(): Promise<void>;
|
|
58
|
-
managedClient(environment: Readonly<Record<string, string
|
|
59
|
+
managedClient(environment: Readonly<Record<string, string>>, isolationKey?: string): OpenCodeServerClient;
|
|
59
60
|
releaseManagedClient(client: OpenCodeServerClient): void;
|
|
60
61
|
}
|
|
@@ -87,6 +87,24 @@ export class OpenCodeRunner extends AbstractRunner {
|
|
|
87
87
|
access: 'default'
|
|
88
88
|
};
|
|
89
89
|
}
|
|
90
|
+
mcpConfiguration(raw, secrets) {
|
|
91
|
+
const servers = super.mcpConfiguration(raw, secrets);
|
|
92
|
+
return Object.fromEntries(servers.map((server) => [
|
|
93
|
+
server.name,
|
|
94
|
+
server.transport === 'STDIO'
|
|
95
|
+
? {
|
|
96
|
+
type: 'local',
|
|
97
|
+
command: [server.command, ...(server.args ?? [])],
|
|
98
|
+
environment: { ...(server.environment ?? {}) }
|
|
99
|
+
}
|
|
100
|
+
: {
|
|
101
|
+
type: 'remote',
|
|
102
|
+
url: server.url,
|
|
103
|
+
headers: { ...(server.headers ?? {}) },
|
|
104
|
+
oauth: false
|
|
105
|
+
}
|
|
106
|
+
]));
|
|
107
|
+
}
|
|
90
108
|
async discoverSessions(context, workspace) {
|
|
91
109
|
const result = [];
|
|
92
110
|
for (const target of workspace === undefined ? context.workspaces : [workspace]) {
|
|
@@ -270,11 +288,13 @@ export class OpenCodeRunner extends AbstractRunner {
|
|
|
270
288
|
await Promise.allSettled(stopping);
|
|
271
289
|
this.stoppingClients.clear();
|
|
272
290
|
}
|
|
273
|
-
managedClient(environment) {
|
|
291
|
+
managedClient(environment, isolationKey) {
|
|
274
292
|
const entries = Object.entries(environment).sort(([left], [right]) => left.localeCompare(right));
|
|
275
|
-
if (entries.length === 0)
|
|
293
|
+
if (entries.length === 0 && isolationKey === undefined)
|
|
276
294
|
return this.client;
|
|
277
|
-
const fingerprint = createHash('sha256')
|
|
295
|
+
const fingerprint = createHash('sha256')
|
|
296
|
+
.update(JSON.stringify({ entries, isolationKey }))
|
|
297
|
+
.digest('hex');
|
|
278
298
|
const existing = this.environmentClients.get(fingerprint);
|
|
279
299
|
if (existing !== undefined) {
|
|
280
300
|
existing.leases += 1;
|
|
@@ -10,7 +10,7 @@ export declare class RunnerRegistry {
|
|
|
10
10
|
product(name: unknown): AbstractRunner<RunnerName> | undefined;
|
|
11
11
|
profiles(capabilities: NodeCapabilities): readonly RunnerProfile[];
|
|
12
12
|
refreshProfiles(capabilities: NodeCapabilities, workspace?: import('../database.js').NodeWorkspace, environment?: Readonly<Record<string, string>>): Promise<readonly RunnerProfile[]>;
|
|
13
|
-
supportsConfiguration(configuration: RunnerDefaultConfiguration, workspace?: import('../database.js').NodeWorkspace): boolean;
|
|
13
|
+
supportsConfiguration(configuration: RunnerDefaultConfiguration, workspace?: import('../database.js').NodeWorkspace, environment?: Readonly<Record<string, string>>): boolean;
|
|
14
14
|
startManagedRun(envelope: NodeEnvelope): boolean;
|
|
15
15
|
cancelManagedRun(runId: string, intent: RunnerCancellationIntent): boolean;
|
|
16
16
|
decideApproval(input: RunnerApprovalDecision): boolean;
|
|
@@ -38,8 +38,8 @@ export class RunnerRegistry {
|
|
|
38
38
|
const profiles = await Promise.all(this.advertised().map((runner) => runner.refreshProfile(capabilities, workspace, environment)));
|
|
39
39
|
return profiles.flatMap((profile) => (profile === undefined ? [] : [profile]));
|
|
40
40
|
}
|
|
41
|
-
supportsConfiguration(configuration, workspace) {
|
|
42
|
-
return (this.product(configuration.runner)?.supportsConfiguration(configuration, workspace) === true);
|
|
41
|
+
supportsConfiguration(configuration, workspace, environment) {
|
|
42
|
+
return (this.product(configuration.runner)?.supportsConfiguration(configuration, workspace, environment) === true);
|
|
43
43
|
}
|
|
44
44
|
startManagedRun(envelope) {
|
|
45
45
|
const runnerName = envelope.payload.runner;
|
package/dist/runner-profiles.js
CHANGED
|
@@ -1,38 +1,18 @@
|
|
|
1
|
-
// Codex reasoning efforts follow the official Codex model surface. Keep this
|
|
2
|
-
// shared so an alias such as GPT-5.6 Luna cannot accidentally lose `high`.
|
|
3
|
-
const CODEX_REASONING_EFFORTS = ['low', 'medium', 'high', 'xhigh'];
|
|
4
1
|
const CLAUDE_MODEL_ENVIRONMENT = {
|
|
5
2
|
opus: 'ANTHROPIC_DEFAULT_OPUS_MODEL',
|
|
6
3
|
sonnet: 'ANTHROPIC_DEFAULT_SONNET_MODEL',
|
|
7
4
|
haiku: 'ANTHROPIC_DEFAULT_HAIKU_MODEL'
|
|
8
5
|
};
|
|
9
6
|
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* the
|
|
13
|
-
*
|
|
14
|
-
* coupling the UI to a vendor version suffix.
|
|
7
|
+
* Keep product-visible permission modes and slash commands in one Node-owned
|
|
8
|
+
* definition. Codex models are supplied by App Server `model/list`; Claude
|
|
9
|
+
* aliases are accepted by the Agent SDK and avoid coupling the UI to a vendor
|
|
10
|
+
* version suffix.
|
|
15
11
|
*/
|
|
16
12
|
const definitions = {
|
|
17
13
|
codex: {
|
|
18
14
|
runner: 'codex',
|
|
19
|
-
models: [
|
|
20
|
-
{
|
|
21
|
-
id: 'gpt-5.6-sol',
|
|
22
|
-
label: 'GPT-5.6 Sol',
|
|
23
|
-
supportedEfforts: [...CODEX_REASONING_EFFORTS]
|
|
24
|
-
},
|
|
25
|
-
{
|
|
26
|
-
id: 'gpt-5.6-terra',
|
|
27
|
-
label: 'GPT-5.6 Terra',
|
|
28
|
-
supportedEfforts: [...CODEX_REASONING_EFFORTS]
|
|
29
|
-
},
|
|
30
|
-
{
|
|
31
|
-
id: 'gpt-5.6-luna',
|
|
32
|
-
label: 'GPT-5.6 Luna',
|
|
33
|
-
supportedEfforts: [...CODEX_REASONING_EFFORTS]
|
|
34
|
-
}
|
|
35
|
-
],
|
|
15
|
+
models: [],
|
|
36
16
|
accessOptions: [
|
|
37
17
|
{ id: 'on-request', label: 'On-request', description: '由 Codex 在需要额外权限时请求确认。' },
|
|
38
18
|
{
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
const execFileAsync = promisify(execFile);
|
|
4
|
+
const COMMANDS = [
|
|
5
|
+
{ id: 'npx', candidates: ['npx'], versionArguments: ['--version'] },
|
|
6
|
+
{ id: 'git', candidates: ['git'], versionArguments: ['--version'] },
|
|
7
|
+
{ id: 'python', candidates: ['python3', 'python'], versionArguments: ['--version'] },
|
|
8
|
+
{ id: 'pip', candidates: ['pip3', 'pip'], versionArguments: ['--version'] }
|
|
9
|
+
];
|
|
10
|
+
export async function detectRuntimeCommands() {
|
|
11
|
+
return Promise.all(COMMANDS.map(detectCommand));
|
|
12
|
+
}
|
|
13
|
+
async function detectCommand(descriptor) {
|
|
14
|
+
for (const command of descriptor.candidates) {
|
|
15
|
+
try {
|
|
16
|
+
const [{ stdout, stderr }, path] = await Promise.all([
|
|
17
|
+
executeVersion(command, descriptor.versionArguments),
|
|
18
|
+
resolveCommandPath(command)
|
|
19
|
+
]);
|
|
20
|
+
const version = normalizeVersion(descriptor.id, `${stdout}${stderr}`.trim().split(/\r?\n/u)[0]?.trim() ?? '');
|
|
21
|
+
return {
|
|
22
|
+
id: descriptor.id,
|
|
23
|
+
command,
|
|
24
|
+
available: true,
|
|
25
|
+
...(version === undefined || version === '' ? {} : { version }),
|
|
26
|
+
...(path === undefined ? {} : { path })
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
// Try the platform-compatible fallback command name.
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
id: descriptor.id,
|
|
35
|
+
command: descriptor.candidates[0],
|
|
36
|
+
available: false
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
export function normalizeVersion(id, value) {
|
|
40
|
+
const normalized = value.trim();
|
|
41
|
+
if (normalized === '')
|
|
42
|
+
return undefined;
|
|
43
|
+
const patterns = {
|
|
44
|
+
git: /^git version\s+(\S+)/iu,
|
|
45
|
+
python: /^python\s+(\S+)/iu,
|
|
46
|
+
pip: /^pip\s+(\S+)/iu
|
|
47
|
+
};
|
|
48
|
+
const match = patterns[id]?.exec(normalized);
|
|
49
|
+
return (match?.[1] ?? normalized).slice(0, 512);
|
|
50
|
+
}
|
|
51
|
+
async function executeVersion(command, args) {
|
|
52
|
+
const executable = process.platform === 'win32' ? process.env['ComSpec'] || 'cmd.exe' : command;
|
|
53
|
+
const commandArgs = process.platform === 'win32'
|
|
54
|
+
? ['/d', '/s', '/c', [command, ...args].map(quoteWindowsArgument).join(' ')]
|
|
55
|
+
: [...args];
|
|
56
|
+
return execFileAsync(executable, commandArgs, {
|
|
57
|
+
timeout: 5_000,
|
|
58
|
+
maxBuffer: 64 * 1024,
|
|
59
|
+
windowsHide: true
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
function quoteWindowsArgument(value) {
|
|
63
|
+
return `"${value.replaceAll('"', '""')}"`;
|
|
64
|
+
}
|
|
65
|
+
async function resolveCommandPath(command) {
|
|
66
|
+
try {
|
|
67
|
+
const resolver = process.platform === 'win32' ? 'where.exe' : 'which';
|
|
68
|
+
const { stdout } = await execFileAsync(resolver, [command], {
|
|
69
|
+
timeout: 2_000,
|
|
70
|
+
maxBuffer: 64 * 1024,
|
|
71
|
+
windowsHide: true
|
|
72
|
+
});
|
|
73
|
+
return stdout.trim().split(/\r?\n/u)[0]?.trim().slice(0, 2048) || undefined;
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { McpCatalogEntry, McpInstallationConfiguration } from '@myagentroam/protocol';
|
|
2
|
+
export declare class McpInstallationVerifier {
|
|
3
|
+
verify(input: {
|
|
4
|
+
readonly entry: McpCatalogEntry;
|
|
5
|
+
readonly configuration: McpInstallationConfiguration;
|
|
6
|
+
readonly secrets: Readonly<Record<string, string>>;
|
|
7
|
+
readonly cwd: string;
|
|
8
|
+
}): Promise<void>;
|
|
9
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
2
|
+
import { getDefaultEnvironment, StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
3
|
+
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
|
4
|
+
const INSTALL_TIMEOUT_MS = 90_000;
|
|
5
|
+
export class McpInstallationVerifier {
|
|
6
|
+
async verify(input) {
|
|
7
|
+
const client = new Client({ name: 'myagentroam-node-installer', version: '0.9.0' });
|
|
8
|
+
try {
|
|
9
|
+
await withTimeout((async () => {
|
|
10
|
+
if (input.entry.runtime.transport === 'STDIO') {
|
|
11
|
+
await client.connect(new StdioClientTransport({
|
|
12
|
+
command: input.entry.runtime.command,
|
|
13
|
+
args: [...input.entry.runtime.args, ...(input.configuration.arguments ?? [])],
|
|
14
|
+
env: {
|
|
15
|
+
...getDefaultEnvironment(),
|
|
16
|
+
...resolveReferences(input.entry.runtime.environment, input.secrets),
|
|
17
|
+
...resolveReferences(input.configuration.environment, input.secrets)
|
|
18
|
+
},
|
|
19
|
+
cwd: input.cwd,
|
|
20
|
+
stderr: 'pipe'
|
|
21
|
+
}));
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
await client.connect(new StreamableHTTPClientTransport(new URL(input.entry.runtime.url), {
|
|
25
|
+
requestInit: {
|
|
26
|
+
headers: {
|
|
27
|
+
...resolveReferences(input.entry.runtime.headers, input.secrets),
|
|
28
|
+
...resolveReferences(input.configuration.headers, input.secrets)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}));
|
|
32
|
+
}
|
|
33
|
+
const tools = await client.listTools();
|
|
34
|
+
if (tools.tools.length === 0)
|
|
35
|
+
throw new Error('MCP_TOOLS_UNAVAILABLE');
|
|
36
|
+
})());
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
throw installError(error);
|
|
40
|
+
}
|
|
41
|
+
finally {
|
|
42
|
+
await Promise.race([client.close().catch(() => undefined), delay(2_000)]);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function resolveReferences(references, secrets) {
|
|
47
|
+
const result = {};
|
|
48
|
+
for (const [name, reference] of Object.entries(references ?? {})) {
|
|
49
|
+
const value = secrets[reference.secretName];
|
|
50
|
+
if (value === undefined)
|
|
51
|
+
throw new Error('MCP_SECRET_MISSING');
|
|
52
|
+
result[name] = value;
|
|
53
|
+
}
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
async function withTimeout(operation) {
|
|
57
|
+
let timer;
|
|
58
|
+
try {
|
|
59
|
+
await Promise.race([
|
|
60
|
+
operation,
|
|
61
|
+
new Promise((_resolve, reject) => {
|
|
62
|
+
timer = setTimeout(() => reject(new Error('MCP_INSTALL_TIMEOUT')), INSTALL_TIMEOUT_MS);
|
|
63
|
+
})
|
|
64
|
+
]);
|
|
65
|
+
}
|
|
66
|
+
finally {
|
|
67
|
+
if (timer !== undefined)
|
|
68
|
+
clearTimeout(timer);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function installError(error) {
|
|
72
|
+
const message = error instanceof Error ? error.message : '';
|
|
73
|
+
if (message === 'MCP_INSTALL_TIMEOUT' ||
|
|
74
|
+
message === 'MCP_SECRET_MISSING' ||
|
|
75
|
+
message === 'MCP_TOOLS_UNAVAILABLE')
|
|
76
|
+
return new Error(message);
|
|
77
|
+
if (/401|403|unauthorized|forbidden/iu.test(message))
|
|
78
|
+
return new Error('MCP_AUTH_FAILED');
|
|
79
|
+
if (/ENOENT|spawn/iu.test(message))
|
|
80
|
+
return new Error('MCP_COMMAND_UNAVAILABLE');
|
|
81
|
+
if (/fetch|connect|network|ECONN|ENOTFOUND/iu.test(message))
|
|
82
|
+
return new Error('MCP_CONNECTION_FAILED');
|
|
83
|
+
return new Error('MCP_PROTOCOL_VALIDATION_FAILED');
|
|
84
|
+
}
|
|
85
|
+
function delay(milliseconds) {
|
|
86
|
+
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
87
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { NodeDatabase } from '../database.js';
|
|
2
|
+
import { McpInstallationVerifier } from './mcp-installation-verifier.js';
|
|
3
|
+
export declare class McpNodeOperationService {
|
|
4
|
+
private readonly requireDatabase;
|
|
5
|
+
private readonly verifier;
|
|
6
|
+
constructor(requireDatabase: () => NodeDatabase, verifier?: McpInstallationVerifier);
|
|
7
|
+
operations(): Readonly<Record<string, (data: unknown) => unknown>>;
|
|
8
|
+
private inspect;
|
|
9
|
+
private install;
|
|
10
|
+
private remove;
|
|
11
|
+
}
|