@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.
- 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/config.d.ts +3 -5
- package/dist/config.js +9 -20
- package/dist/connector/node-connector-options.d.ts +1 -0
- package/dist/connector.js +12 -3
- package/dist/database.d.ts +23 -0
- package/dist/database.js +84 -2
- package/dist/main.js +4 -1
- 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 +13 -1
- package/dist/operational.js +4 -1
- package/dist/rotating-log.d.ts +17 -0
- package/dist/rotating-log.js +67 -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/node-connection-lifecycle-service.js +1 -2
- package/dist/service/runner-service.d.ts +4 -2
- package/dist/service/runner-service.js +5 -2
- package/dist/service/session-lifecycle-service.js +7 -4
- package/dist/service/skill-directory-service.d.ts +17 -0
- package/dist/service/skill-directory-service.js +203 -0
- package/dist/service/skill-install-service.d.ts +21 -0
- package/dist/service/skill-install-service.js +504 -0
- package/dist/service/skill-node-operation-service.d.ts +44 -0
- package/dist/service/skill-node-operation-service.js +203 -0
- package/dist/service/workbench-manifest-service.d.ts +4 -2
- package/dist/service/workspace-queue-workbench-service.d.ts +2 -0
- package/dist/service/workspace-queue-workbench-service.js +2 -1
- package/dist/supervisor.js +1 -20
- 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: '
|
|
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: '
|
|
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
|
-
|
|
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
|
-
|
|
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;
|