@myagentroam/node 0.1.7 → 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 (56) 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/config.d.ts +3 -5
  7. package/dist/config.js +9 -20
  8. package/dist/connector/node-connector-options.d.ts +1 -0
  9. package/dist/connector.js +12 -3
  10. package/dist/database.d.ts +23 -0
  11. package/dist/database.js +84 -2
  12. package/dist/main.js +4 -1
  13. package/dist/migrations/v004.d.ts +4 -0
  14. package/dist/migrations/v004.js +35 -0
  15. package/dist/opencode-server.d.ts +1 -0
  16. package/dist/opencode-server.js +13 -1
  17. package/dist/operational.js +4 -1
  18. package/dist/rotating-log.d.ts +17 -0
  19. package/dist/rotating-log.js +67 -0
  20. package/dist/runner/abstract-runner.d.ts +12 -2
  21. package/dist/runner/abstract-runner.js +76 -2
  22. package/dist/runner/claude/managed-run-controller.js +2 -1
  23. package/dist/runner/claude-code-runner.d.ts +1 -0
  24. package/dist/runner/claude-code-runner.js +14 -0
  25. package/dist/runner/codex/managed-run-controller.js +5 -3
  26. package/dist/runner/codex-runner.d.ts +16 -2
  27. package/dist/runner/codex-runner.js +215 -4
  28. package/dist/runner/opencode/managed-run-controller.js +13 -3
  29. package/dist/runner/opencode-runner.d.ts +2 -1
  30. package/dist/runner/opencode-runner.js +23 -3
  31. package/dist/runner/runner-registry.d.ts +1 -1
  32. package/dist/runner/runner-registry.js +2 -2
  33. package/dist/runner-profiles.js +5 -25
  34. package/dist/runtime-command-detector.d.ts +3 -0
  35. package/dist/runtime-command-detector.js +78 -0
  36. package/dist/service/mcp-installation-verifier.d.ts +9 -0
  37. package/dist/service/mcp-installation-verifier.js +87 -0
  38. package/dist/service/mcp-node-operation-service.d.ts +11 -0
  39. package/dist/service/mcp-node-operation-service.js +90 -0
  40. package/dist/service/mcp-package-installer.d.ts +8 -0
  41. package/dist/service/mcp-package-installer.js +136 -0
  42. package/dist/service/node-connection-lifecycle-service.js +1 -2
  43. package/dist/service/runner-service.d.ts +4 -2
  44. package/dist/service/runner-service.js +5 -2
  45. package/dist/service/session-lifecycle-service.js +7 -4
  46. package/dist/service/skill-directory-service.d.ts +17 -0
  47. package/dist/service/skill-directory-service.js +203 -0
  48. package/dist/service/skill-install-service.d.ts +21 -0
  49. package/dist/service/skill-install-service.js +504 -0
  50. package/dist/service/skill-node-operation-service.d.ts +44 -0
  51. package/dist/service/skill-node-operation-service.js +203 -0
  52. package/dist/service/workbench-manifest-service.d.ts +4 -2
  53. package/dist/service/workspace-queue-workbench-service.d.ts +2 -0
  54. package/dist/service/workspace-queue-workbench-service.js +2 -1
  55. package/dist/supervisor.js +1 -20
  56. package/package.json +2 -2
@@ -21,14 +21,15 @@ export declare abstract class AbstractRunner<TName extends RegisteredRunnerName
21
21
  abstract profile(capabilities: NodeCapabilities): RunnerProfile | undefined;
22
22
  refreshProfile(capabilities: NodeCapabilities, workspace?: NodeWorkspace, environment?: Readonly<Record<string, string>>): Promise<RunnerProfile | undefined>;
23
23
  abstract available(capabilities: NodeCapabilities): boolean;
24
- abstract defaultConfiguration(workspace?: NodeWorkspace): RunnerDefaultConfiguration;
25
- supportsConfiguration(configuration: RunnerDefaultConfiguration, workspace?: NodeWorkspace): boolean;
24
+ abstract defaultConfiguration(workspace?: NodeWorkspace, environment?: Readonly<Record<string, string>>): RunnerDefaultConfiguration;
25
+ supportsConfiguration(configuration: RunnerDefaultConfiguration, workspace?: NodeWorkspace, environment?: Readonly<Record<string, string>>): boolean;
26
26
  protected abstract profileForValidation(): RunnerProfile;
27
27
  abstract resumeExternal(session: NodeAgentSession, context: ExternalResumeContext, force?: boolean): Promise<ExternalResumeResult>;
28
28
  abstract discoverSessions(context: RunnerDiscoveryContext, workspace?: NodeWorkspace): Promise<readonly NodeAgentSession[]>;
29
29
  abstract readContextUsage(session: NodeAgentSession): Promise<RunnerContextUsage | undefined>;
30
30
  presentSession(session: NodeAgentSession, capabilities: NodeCapabilities, context: RunnerSessionPresentationContext): RunnerSessionPresentation;
31
31
  prepareMessageInput(input: string, collaborationMode: 'default' | 'plan'): string;
32
+ mcpConfiguration(rawInstallations: unknown, secretEnvironment: Readonly<Record<string, string>>): unknown;
32
33
  channelToken(sessionId: string): string | undefined;
33
34
  deliverChannel(sessionId: string, messageId: string, senderId: string, token: string, content: string, notify: (runId: string, messageId: string, delivery: ChannelDelivery) => void): ChannelDelivery;
34
35
  executeGlobalCommand(commandId: string, input: string): unknown;
@@ -66,6 +67,15 @@ export declare abstract class AbstractRunner<TName extends RegisteredRunnerName
66
67
  readonly secretEnvironment: Readonly<Record<string, string>>;
67
68
  }): Promise<unknown>;
68
69
  }
70
+ export interface ResolvedMcpServer {
71
+ readonly name: string;
72
+ readonly transport: 'STDIO' | 'HTTP';
73
+ readonly command?: string;
74
+ readonly args?: readonly string[];
75
+ readonly environment?: Readonly<Record<string, string>>;
76
+ readonly url?: string;
77
+ readonly headers?: Readonly<Record<string, string>>;
78
+ }
69
79
  export type RunnerCancellationIntent = 'CANCEL' | 'INTERRUPT';
70
80
  export interface RunnerControl {
71
81
  start(envelope: NodeEnvelope): void;
@@ -1,3 +1,4 @@
1
+ import { mcpRuntimeDescriptorSchema } from '@myagentroam/protocol';
1
2
  /**
2
3
  * Stable lifecycle boundary for every Node Runner implementation.
3
4
  *
@@ -15,8 +16,8 @@ export class AbstractRunner {
15
16
  void [workspace, environment];
16
17
  return this.profile(capabilities);
17
18
  }
18
- supportsConfiguration(configuration, workspace) {
19
- void workspace;
19
+ supportsConfiguration(configuration, workspace, environment) {
20
+ void [workspace, environment];
20
21
  const profile = this.profileForValidation();
21
22
  const model = profile.models.find((candidate) => candidate.id === configuration.model);
22
23
  return (configuration.runner === this.name &&
@@ -32,6 +33,9 @@ export class AbstractRunner {
32
33
  void collaborationMode;
33
34
  return input;
34
35
  }
36
+ mcpConfiguration(rawInstallations, secretEnvironment) {
37
+ return resolvedMcpServers(rawInstallations, secretEnvironment);
38
+ }
35
39
  channelToken(sessionId) {
36
40
  void sessionId;
37
41
  return undefined;
@@ -143,6 +147,76 @@ export class AbstractRunner {
143
147
  return this.commandControl.execute(session, command, input, context);
144
148
  }
145
149
  }
150
+ function resolvedMcpServers(raw, secrets) {
151
+ if (!Array.isArray(raw))
152
+ return [];
153
+ const result = [];
154
+ for (const value of raw) {
155
+ if (value === null || typeof value !== 'object' || Array.isArray(value))
156
+ continue;
157
+ const record = value;
158
+ if (typeof record['serverName'] !== 'string' || record['enabled'] !== true)
159
+ continue;
160
+ const runtime = mcpRuntimeDescriptorSchema.safeParse(record['runtime']);
161
+ if (!runtime.success)
162
+ continue;
163
+ const configuration = isRecord(record['configuration']) ? record['configuration'] : {};
164
+ const resolveReferences = (candidate) => {
165
+ if (!isRecord(candidate))
166
+ return {};
167
+ const result = {};
168
+ for (const [name, reference] of Object.entries(candidate)) {
169
+ if (!isRecord(reference) || typeof reference['secretName'] !== 'string')
170
+ return undefined;
171
+ const secret = secrets[reference['secretName']];
172
+ if (secret === undefined)
173
+ return undefined;
174
+ result[name] = secret;
175
+ }
176
+ return result;
177
+ };
178
+ if (runtime.data.transport === 'STDIO') {
179
+ const runtimeEnvironment = resolveReferences(runtime.data.environment);
180
+ const configuredEnvironment = resolveReferences(configuration['environment']);
181
+ if (runtimeEnvironment === undefined || configuredEnvironment === undefined)
182
+ continue;
183
+ result.push({
184
+ name: record['serverName'],
185
+ transport: 'STDIO',
186
+ command: runtime.data.command,
187
+ args: [
188
+ ...runtime.data.args,
189
+ ...(Array.isArray(configuration['arguments'])
190
+ ? configuration['arguments'].filter((item) => typeof item === 'string')
191
+ : [])
192
+ ],
193
+ environment: {
194
+ ...runtimeEnvironment,
195
+ ...configuredEnvironment
196
+ }
197
+ });
198
+ }
199
+ else {
200
+ const runtimeHeaders = resolveReferences(runtime.data.headers);
201
+ const configuredHeaders = resolveReferences(configuration['headers']);
202
+ if (runtimeHeaders === undefined || configuredHeaders === undefined)
203
+ continue;
204
+ result.push({
205
+ name: record['serverName'],
206
+ transport: 'HTTP',
207
+ url: runtime.data.url,
208
+ headers: {
209
+ ...runtimeHeaders,
210
+ ...configuredHeaders
211
+ }
212
+ });
213
+ }
214
+ }
215
+ return result;
216
+ }
217
+ function isRecord(value) {
218
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
219
+ }
146
220
  export class RunnerCapabilityError extends Error {
147
221
  runner;
148
222
  capability;
@@ -27,7 +27,7 @@ export class ClaudeManagedRunController {
27
27
  detail: '后续消息将以标签式规划模式运行,只读规划并输出计划卡。',
28
28
  closable: true,
29
29
  closeInput: '/plan',
30
- continuation: { label: 'Implement', prompt: '请根据上述计划开始实施。' }
30
+ continuation: { label: '执行', prompt: '请根据上述计划开始实施。' }
31
31
  });
32
32
  return { command: command.id, states: this.host.commandStates(session.id) };
33
33
  }
@@ -138,6 +138,7 @@ export class ClaudeManagedRunController {
138
138
  ...(typeof payload.effort === 'string' ? { effort: payload.effort } : {}),
139
139
  ...(typeof payload.access === 'string' ? { access: payload.access } : {}),
140
140
  environment: secretEnvironment,
141
+ mcpServers: this.runner.mcpConfiguration(payload.mcpInstallations, secretEnvironment),
141
142
  ...(attachments.length === 0
142
143
  ? {}
143
144
  : {
@@ -17,6 +17,7 @@ export declare class ClaudeCodeRunner extends AbstractRunner<'claude-code'> {
17
17
  available(capabilities: NodeCapabilities): boolean;
18
18
  protected profileForValidation(): RunnerProfile;
19
19
  defaultConfiguration(): RunnerDefaultConfiguration;
20
+ mcpConfiguration(raw: unknown, secrets: Readonly<Record<string, string>>): unknown;
20
21
  resumeExternal(external: NodeAgentSession, context: ExternalResumeContext): Promise<ExternalResumeResult>;
21
22
  discoverSessions(context: RunnerDiscoveryContext, workspace?: NodeWorkspace): Promise<readonly NodeAgentSession[]>;
22
23
  readContextUsage(session: NodeAgentSession): Promise<import("../native-session-history.js").NativeClaudeContextUsage | undefined>;
@@ -45,6 +45,20 @@ export class ClaudeCodeRunner extends AbstractRunner {
45
45
  access: 'default'
46
46
  };
47
47
  }
48
+ mcpConfiguration(raw, secrets) {
49
+ const servers = super.mcpConfiguration(raw, secrets);
50
+ return Object.fromEntries(servers.map((server) => [
51
+ server.name,
52
+ server.transport === 'STDIO'
53
+ ? {
54
+ type: 'stdio',
55
+ command: server.command,
56
+ args: [...(server.args ?? [])],
57
+ env: { ...(server.environment ?? {}) }
58
+ }
59
+ : { type: 'http', url: server.url, headers: { ...(server.headers ?? {}) } }
60
+ ]));
61
+ }
48
62
  async resumeExternal(external, context) {
49
63
  if (external.runner !== this.name ||
50
64
  external.nativeControl !== 'EXTERNAL' ||
@@ -35,7 +35,7 @@ export class CodexManagedRunController {
35
35
  detail: '后续消息将以协作规划模式运行。',
36
36
  closable: true,
37
37
  closeInput: '/plan',
38
- continuation: { label: 'Implement', prompt: '请根据上述计划开始实施。' }
38
+ continuation: { label: '执行', prompt: '请根据上述计划开始实施。' }
39
39
  });
40
40
  return { command: command.id, states: this.host.commandStates(session.id) };
41
41
  }
@@ -325,7 +325,8 @@ export class CodexManagedRunController {
325
325
  effort: payload.effort,
326
326
  access: payload.access,
327
327
  collaborationMode: payload.collaborationMode,
328
- serviceTier: payload.serviceTier
328
+ serviceTier: payload.serviceTier,
329
+ mcpServers: this.runner.mcpConfiguration(payload.mcpInstallations, secretEnvironment)
329
330
  }, attachments);
330
331
  }
331
332
  async run(payload, attachments) {
@@ -340,7 +341,8 @@ export class CodexManagedRunController {
340
341
  const configuration = {
341
342
  ...(typeof payload.model === 'string' ? { model: payload.model } : {}),
342
343
  ...(typeof payload.effort === 'string' ? { effort: payload.effort } : {}),
343
- ...(typeof payload.access === 'string' ? { access: payload.access } : {})
344
+ ...(typeof payload.access === 'string' ? { access: payload.access } : {}),
345
+ ...(payload.mcpServers === undefined ? {} : { mcpServers: payload.mcpServers })
344
346
  };
345
347
  const threadResult = typeof payload.externalSessionId === 'string'
346
348
  ? await this.runner.resumeThread(payload.externalSessionId, cwd, configuration)
@@ -5,17 +5,29 @@ import type { NodeAgentSession, NodeDatabase } from '../database.js';
5
5
  import type { NodeWorkspace } from '../database.js';
6
6
  export declare class CodexRunner extends AbstractRunner<'codex'> {
7
7
  private readonly client;
8
+ private readonly profileClientFactory;
8
9
  readonly name: "codex";
9
10
  readonly discoveryScope: "ALL_WORKSPACES";
10
11
  readonly execution: CodexExecutionState;
11
12
  private fastEnabled;
12
13
  private environmentFingerprint;
13
14
  private readonly environmentRuns;
14
- constructor(client?: CodexAppServerClient);
15
+ private readonly profileCache;
16
+ private readonly profileRefreshes;
17
+ private readonly profileClients;
18
+ private readonly profileKeyByEnvironment;
19
+ private latestProfileCache;
20
+ private selectedProfileKey;
21
+ constructor(client?: CodexAppServerClient, profileClientFactory?: () => CodexAppServerClient);
15
22
  profile(capabilities: NodeCapabilities): RunnerProfile;
23
+ refreshProfile(capabilities: NodeCapabilities, workspace?: NodeWorkspace, environment?: Readonly<Record<string, string>>): Promise<RunnerProfile>;
24
+ private refreshCodexProfile;
25
+ private selectCachedProfile;
26
+ private fetchCodexProfile;
16
27
  available(capabilities: NodeCapabilities): boolean;
17
28
  executeGlobalCommand(commandId: string, input: string): unknown;
18
29
  serviceTier(): 'fast' | undefined;
30
+ mcpConfiguration(raw: unknown, secrets: Readonly<Record<string, string>>): unknown;
19
31
  usesRunnerTitleForRename(): boolean;
20
32
  renameNative(session: NodeAgentSession, input: {
21
33
  readonly title?: string | null;
@@ -35,7 +47,9 @@ export declare class CodexRunner extends AbstractRunner<'codex'> {
35
47
  nativeImageRoot(session: NodeAgentSession): string | undefined;
36
48
  managedRunForNativeTurn(nativeTurnId: string): string | undefined;
37
49
  protected profileForValidation(): RunnerProfile;
38
- defaultConfiguration(): RunnerDefaultConfiguration;
50
+ defaultConfiguration(_workspace?: NodeWorkspace, environment?: Readonly<Record<string, string>>): RunnerDefaultConfiguration;
51
+ supportsConfiguration(configuration: RunnerDefaultConfiguration, _workspace?: NodeWorkspace, environment?: Readonly<Record<string, string>>): boolean;
52
+ private profileCacheForEnvironment;
39
53
  resumeExternal(external: NodeAgentSession, context: ExternalResumeContext, force?: boolean): Promise<ExternalResumeResult>;
40
54
  discoverSessions(context: RunnerDiscoveryContext, workspace?: NodeWorkspace): Promise<readonly NodeAgentSession[]>;
41
55
  readContextUsage(session: NodeAgentSession): Promise<import("../native-session-history.js").NativeCodexContextUsage | undefined>;
@@ -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;