@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.
Files changed (42) hide show
  1. package/dist/capabilities.js +6 -3
  2. package/dist/claude-agent-sdk.d.ts +1 -0
  3. package/dist/claude-agent-sdk.js +8 -1
  4. package/dist/codex-app-server.d.ts +6 -0
  5. package/dist/codex-app-server.js +6 -0
  6. package/dist/connector/node-connector-options.d.ts +1 -0
  7. package/dist/connector.js +5 -2
  8. package/dist/database.d.ts +23 -0
  9. package/dist/database.js +84 -2
  10. package/dist/migrations/v004.d.ts +4 -0
  11. package/dist/migrations/v004.js +35 -0
  12. package/dist/opencode-server.d.ts +1 -0
  13. package/dist/opencode-server.js +12 -0
  14. package/dist/runner/abstract-runner.d.ts +12 -2
  15. package/dist/runner/abstract-runner.js +76 -2
  16. package/dist/runner/claude/managed-run-controller.js +2 -1
  17. package/dist/runner/claude-code-runner.d.ts +1 -0
  18. package/dist/runner/claude-code-runner.js +14 -0
  19. package/dist/runner/codex/managed-run-controller.js +5 -3
  20. package/dist/runner/codex-runner.d.ts +16 -2
  21. package/dist/runner/codex-runner.js +215 -4
  22. package/dist/runner/opencode/managed-run-controller.js +13 -3
  23. package/dist/runner/opencode-runner.d.ts +2 -1
  24. package/dist/runner/opencode-runner.js +23 -3
  25. package/dist/runner/runner-registry.d.ts +1 -1
  26. package/dist/runner/runner-registry.js +2 -2
  27. package/dist/runner-profiles.js +5 -25
  28. package/dist/runtime-command-detector.d.ts +3 -0
  29. package/dist/runtime-command-detector.js +78 -0
  30. package/dist/service/mcp-installation-verifier.d.ts +9 -0
  31. package/dist/service/mcp-installation-verifier.js +87 -0
  32. package/dist/service/mcp-node-operation-service.d.ts +11 -0
  33. package/dist/service/mcp-node-operation-service.js +90 -0
  34. package/dist/service/mcp-package-installer.d.ts +8 -0
  35. package/dist/service/mcp-package-installer.js +136 -0
  36. package/dist/service/runner-service.d.ts +2 -0
  37. package/dist/service/runner-service.js +5 -2
  38. package/dist/service/session-lifecycle-service.js +7 -4
  39. package/dist/service/workbench-manifest-service.d.ts +2 -0
  40. package/dist/service/workspace-queue-workbench-service.d.ts +2 -0
  41. package/dist/service/workspace-queue-workbench-service.js +2 -1
  42. 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
- constructor(client = new CodexAppServerClient()) {
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
- return declaredRunnerProfiles({
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 { runner: this.name, model: 'gpt-5.6-sol', effort: 'medium', access: 'on-request' };
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: 'Implement', prompt: '请根据上述计划开始实施。' }
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
- const client = this.runner.managedClient(input.environment);
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>>): OpenCodeServerClient;
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').update(JSON.stringify(entries)).digest('hex');
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;
@@ -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
- * The Runner APIs used by MAR do not expose a portable model-list endpoint.
11
- * Keep the supported, product-visible choices in one Node-owned definition so
12
- * the browser never guesses models, effort levels, permission modes, or slash
13
- * commands. Model aliases for Claude are accepted by the Agent SDK and avoid
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,3 @@
1
+ import type { RuntimeCommand } from '@myagentroam/protocol';
2
+ export declare function detectRuntimeCommands(): Promise<RuntimeCommand[]>;
3
+ export declare function normalizeVersion(id: RuntimeCommand['id'], value: string): string | undefined;
@@ -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
+ }