@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
package/dist/runner-profiles.js
CHANGED
|
@@ -1,38 +1,18 @@
|
|
|
1
|
-
// Codex reasoning efforts follow the official Codex model surface. Keep this
|
|
2
|
-
// shared so an alias such as GPT-5.6 Luna cannot accidentally lose `high`.
|
|
3
|
-
const CODEX_REASONING_EFFORTS = ['low', 'medium', 'high', 'xhigh'];
|
|
4
1
|
const CLAUDE_MODEL_ENVIRONMENT = {
|
|
5
2
|
opus: 'ANTHROPIC_DEFAULT_OPUS_MODEL',
|
|
6
3
|
sonnet: 'ANTHROPIC_DEFAULT_SONNET_MODEL',
|
|
7
4
|
haiku: 'ANTHROPIC_DEFAULT_HAIKU_MODEL'
|
|
8
5
|
};
|
|
9
6
|
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* the
|
|
13
|
-
*
|
|
14
|
-
* coupling the UI to a vendor version suffix.
|
|
7
|
+
* Keep product-visible permission modes and slash commands in one Node-owned
|
|
8
|
+
* definition. Codex models are supplied by App Server `model/list`; Claude
|
|
9
|
+
* aliases are accepted by the Agent SDK and avoid coupling the UI to a vendor
|
|
10
|
+
* version suffix.
|
|
15
11
|
*/
|
|
16
12
|
const definitions = {
|
|
17
13
|
codex: {
|
|
18
14
|
runner: 'codex',
|
|
19
|
-
models: [
|
|
20
|
-
{
|
|
21
|
-
id: 'gpt-5.6-sol',
|
|
22
|
-
label: 'GPT-5.6 Sol',
|
|
23
|
-
supportedEfforts: [...CODEX_REASONING_EFFORTS]
|
|
24
|
-
},
|
|
25
|
-
{
|
|
26
|
-
id: 'gpt-5.6-terra',
|
|
27
|
-
label: 'GPT-5.6 Terra',
|
|
28
|
-
supportedEfforts: [...CODEX_REASONING_EFFORTS]
|
|
29
|
-
},
|
|
30
|
-
{
|
|
31
|
-
id: 'gpt-5.6-luna',
|
|
32
|
-
label: 'GPT-5.6 Luna',
|
|
33
|
-
supportedEfforts: [...CODEX_REASONING_EFFORTS]
|
|
34
|
-
}
|
|
35
|
-
],
|
|
15
|
+
models: [],
|
|
36
16
|
accessOptions: [
|
|
37
17
|
{ id: 'on-request', label: 'On-request', description: '由 Codex 在需要额外权限时请求确认。' },
|
|
38
18
|
{
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
const execFileAsync = promisify(execFile);
|
|
4
|
+
const COMMANDS = [
|
|
5
|
+
{ id: 'npx', candidates: ['npx'], versionArguments: ['--version'] },
|
|
6
|
+
{ id: 'git', candidates: ['git'], versionArguments: ['--version'] },
|
|
7
|
+
{ id: 'python', candidates: ['python3', 'python'], versionArguments: ['--version'] },
|
|
8
|
+
{ id: 'pip', candidates: ['pip3', 'pip'], versionArguments: ['--version'] }
|
|
9
|
+
];
|
|
10
|
+
export async function detectRuntimeCommands() {
|
|
11
|
+
return Promise.all(COMMANDS.map(detectCommand));
|
|
12
|
+
}
|
|
13
|
+
async function detectCommand(descriptor) {
|
|
14
|
+
for (const command of descriptor.candidates) {
|
|
15
|
+
try {
|
|
16
|
+
const [{ stdout, stderr }, path] = await Promise.all([
|
|
17
|
+
executeVersion(command, descriptor.versionArguments),
|
|
18
|
+
resolveCommandPath(command)
|
|
19
|
+
]);
|
|
20
|
+
const version = normalizeVersion(descriptor.id, `${stdout}${stderr}`.trim().split(/\r?\n/u)[0]?.trim() ?? '');
|
|
21
|
+
return {
|
|
22
|
+
id: descriptor.id,
|
|
23
|
+
command,
|
|
24
|
+
available: true,
|
|
25
|
+
...(version === undefined || version === '' ? {} : { version }),
|
|
26
|
+
...(path === undefined ? {} : { path })
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
// Try the platform-compatible fallback command name.
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
id: descriptor.id,
|
|
35
|
+
command: descriptor.candidates[0],
|
|
36
|
+
available: false
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
export function normalizeVersion(id, value) {
|
|
40
|
+
const normalized = value.trim();
|
|
41
|
+
if (normalized === '')
|
|
42
|
+
return undefined;
|
|
43
|
+
const patterns = {
|
|
44
|
+
git: /^git version\s+(\S+)/iu,
|
|
45
|
+
python: /^python\s+(\S+)/iu,
|
|
46
|
+
pip: /^pip\s+(\S+)/iu
|
|
47
|
+
};
|
|
48
|
+
const match = patterns[id]?.exec(normalized);
|
|
49
|
+
return (match?.[1] ?? normalized).slice(0, 512);
|
|
50
|
+
}
|
|
51
|
+
async function executeVersion(command, args) {
|
|
52
|
+
const executable = process.platform === 'win32' ? process.env['ComSpec'] || 'cmd.exe' : command;
|
|
53
|
+
const commandArgs = process.platform === 'win32'
|
|
54
|
+
? ['/d', '/s', '/c', [command, ...args].map(quoteWindowsArgument).join(' ')]
|
|
55
|
+
: [...args];
|
|
56
|
+
return execFileAsync(executable, commandArgs, {
|
|
57
|
+
timeout: 5_000,
|
|
58
|
+
maxBuffer: 64 * 1024,
|
|
59
|
+
windowsHide: true
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
function quoteWindowsArgument(value) {
|
|
63
|
+
return `"${value.replaceAll('"', '""')}"`;
|
|
64
|
+
}
|
|
65
|
+
async function resolveCommandPath(command) {
|
|
66
|
+
try {
|
|
67
|
+
const resolver = process.platform === 'win32' ? 'where.exe' : 'which';
|
|
68
|
+
const { stdout } = await execFileAsync(resolver, [command], {
|
|
69
|
+
timeout: 2_000,
|
|
70
|
+
maxBuffer: 64 * 1024,
|
|
71
|
+
windowsHide: true
|
|
72
|
+
});
|
|
73
|
+
return stdout.trim().split(/\r?\n/u)[0]?.trim().slice(0, 2048) || undefined;
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { McpCatalogEntry, McpInstallationConfiguration } from '@myagentroam/protocol';
|
|
2
|
+
export declare class McpInstallationVerifier {
|
|
3
|
+
verify(input: {
|
|
4
|
+
readonly entry: McpCatalogEntry;
|
|
5
|
+
readonly configuration: McpInstallationConfiguration;
|
|
6
|
+
readonly secrets: Readonly<Record<string, string>>;
|
|
7
|
+
readonly cwd: string;
|
|
8
|
+
}): Promise<void>;
|
|
9
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
2
|
+
import { getDefaultEnvironment, StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
3
|
+
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
|
4
|
+
const INSTALL_TIMEOUT_MS = 90_000;
|
|
5
|
+
export class McpInstallationVerifier {
|
|
6
|
+
async verify(input) {
|
|
7
|
+
const client = new Client({ name: 'myagentroam-node-installer', version: '0.9.0' });
|
|
8
|
+
try {
|
|
9
|
+
await withTimeout((async () => {
|
|
10
|
+
if (input.entry.runtime.transport === 'STDIO') {
|
|
11
|
+
await client.connect(new StdioClientTransport({
|
|
12
|
+
command: input.entry.runtime.command,
|
|
13
|
+
args: [...input.entry.runtime.args, ...(input.configuration.arguments ?? [])],
|
|
14
|
+
env: {
|
|
15
|
+
...getDefaultEnvironment(),
|
|
16
|
+
...resolveReferences(input.entry.runtime.environment, input.secrets),
|
|
17
|
+
...resolveReferences(input.configuration.environment, input.secrets)
|
|
18
|
+
},
|
|
19
|
+
cwd: input.cwd,
|
|
20
|
+
stderr: 'pipe'
|
|
21
|
+
}));
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
await client.connect(new StreamableHTTPClientTransport(new URL(input.entry.runtime.url), {
|
|
25
|
+
requestInit: {
|
|
26
|
+
headers: {
|
|
27
|
+
...resolveReferences(input.entry.runtime.headers, input.secrets),
|
|
28
|
+
...resolveReferences(input.configuration.headers, input.secrets)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}));
|
|
32
|
+
}
|
|
33
|
+
const tools = await client.listTools();
|
|
34
|
+
if (tools.tools.length === 0)
|
|
35
|
+
throw new Error('MCP_TOOLS_UNAVAILABLE');
|
|
36
|
+
})());
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
throw installError(error);
|
|
40
|
+
}
|
|
41
|
+
finally {
|
|
42
|
+
await Promise.race([client.close().catch(() => undefined), delay(2_000)]);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function resolveReferences(references, secrets) {
|
|
47
|
+
const result = {};
|
|
48
|
+
for (const [name, reference] of Object.entries(references ?? {})) {
|
|
49
|
+
const value = secrets[reference.secretName];
|
|
50
|
+
if (value === undefined)
|
|
51
|
+
throw new Error('MCP_SECRET_MISSING');
|
|
52
|
+
result[name] = value;
|
|
53
|
+
}
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
async function withTimeout(operation) {
|
|
57
|
+
let timer;
|
|
58
|
+
try {
|
|
59
|
+
await Promise.race([
|
|
60
|
+
operation,
|
|
61
|
+
new Promise((_resolve, reject) => {
|
|
62
|
+
timer = setTimeout(() => reject(new Error('MCP_INSTALL_TIMEOUT')), INSTALL_TIMEOUT_MS);
|
|
63
|
+
})
|
|
64
|
+
]);
|
|
65
|
+
}
|
|
66
|
+
finally {
|
|
67
|
+
if (timer !== undefined)
|
|
68
|
+
clearTimeout(timer);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function installError(error) {
|
|
72
|
+
const message = error instanceof Error ? error.message : '';
|
|
73
|
+
if (message === 'MCP_INSTALL_TIMEOUT' ||
|
|
74
|
+
message === 'MCP_SECRET_MISSING' ||
|
|
75
|
+
message === 'MCP_TOOLS_UNAVAILABLE')
|
|
76
|
+
return new Error(message);
|
|
77
|
+
if (/401|403|unauthorized|forbidden/iu.test(message))
|
|
78
|
+
return new Error('MCP_AUTH_FAILED');
|
|
79
|
+
if (/ENOENT|spawn/iu.test(message))
|
|
80
|
+
return new Error('MCP_COMMAND_UNAVAILABLE');
|
|
81
|
+
if (/fetch|connect|network|ECONN|ENOTFOUND/iu.test(message))
|
|
82
|
+
return new Error('MCP_CONNECTION_FAILED');
|
|
83
|
+
return new Error('MCP_PROTOCOL_VALIDATION_FAILED');
|
|
84
|
+
}
|
|
85
|
+
function delay(milliseconds) {
|
|
86
|
+
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
87
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { NodeDatabase } from '../database.js';
|
|
2
|
+
import { McpInstallationVerifier } from './mcp-installation-verifier.js';
|
|
3
|
+
export declare class McpNodeOperationService {
|
|
4
|
+
private readonly requireDatabase;
|
|
5
|
+
private readonly verifier;
|
|
6
|
+
constructor(requireDatabase: () => NodeDatabase, verifier?: McpInstallationVerifier);
|
|
7
|
+
operations(): Readonly<Record<string, (data: unknown) => unknown>>;
|
|
8
|
+
private inspect;
|
|
9
|
+
private install;
|
|
10
|
+
private remove;
|
|
11
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { mcpCatalogEntrySchema, mcpInstallationInputSchema, mcpTargetInspectionSchema } from '@myagentroam/protocol';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { McpInstallationVerifier } from './mcp-installation-verifier.js';
|
|
4
|
+
export class McpNodeOperationService {
|
|
5
|
+
requireDatabase;
|
|
6
|
+
verifier;
|
|
7
|
+
constructor(requireDatabase, verifier = new McpInstallationVerifier()) {
|
|
8
|
+
this.requireDatabase = requireDatabase;
|
|
9
|
+
this.verifier = verifier;
|
|
10
|
+
}
|
|
11
|
+
operations() {
|
|
12
|
+
return {
|
|
13
|
+
'node.mcps.inspect': () => this.inspect('NODE', ''),
|
|
14
|
+
'node.mcps.install': (data) => this.install('NODE', '', data),
|
|
15
|
+
'node.mcps.remove': (data) => this.remove('NODE', '', data),
|
|
16
|
+
'workspace.mcps.inspect': (data) => this.inspect('WORKSPACE', workspaceId(data)),
|
|
17
|
+
'workspace.mcps.install': (data) => this.install('WORKSPACE', workspaceId(data), data),
|
|
18
|
+
'workspace.mcps.remove': (data) => this.remove('WORKSPACE', workspaceId(data), data)
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
inspect(targetKind, targetId) {
|
|
22
|
+
const database = this.requireDatabase();
|
|
23
|
+
const own = database.listMcpInstallations(targetKind, targetId);
|
|
24
|
+
const ownNames = new Set(own.map((installation) => installation.serverName));
|
|
25
|
+
const installations = [
|
|
26
|
+
...(targetKind === 'WORKSPACE'
|
|
27
|
+
? database
|
|
28
|
+
.listMcpInstallations('NODE')
|
|
29
|
+
.filter((installation) => !ownNames.has(installation.serverName))
|
|
30
|
+
: []),
|
|
31
|
+
...own
|
|
32
|
+
].map((installation) => ({
|
|
33
|
+
catalogEntryId: installation.catalogEntryId,
|
|
34
|
+
serverName: installation.serverName,
|
|
35
|
+
displayName: installation.displayName,
|
|
36
|
+
version: installation.version,
|
|
37
|
+
origin: installation.targetKind,
|
|
38
|
+
enabled: installation.enabled,
|
|
39
|
+
status: 'CURRENT',
|
|
40
|
+
installedAt: installation.installedAt
|
|
41
|
+
}));
|
|
42
|
+
return mcpTargetInspectionSchema.parse({ targetKind, installations });
|
|
43
|
+
}
|
|
44
|
+
async install(targetKind, targetId, raw) {
|
|
45
|
+
if (!isRecord(raw))
|
|
46
|
+
throw new Error('MCP_INSTALL_INPUT_INVALID');
|
|
47
|
+
const entry = mcpCatalogEntrySchema.parse(raw['entry']);
|
|
48
|
+
const input = mcpInstallationInputSchema.parse({
|
|
49
|
+
catalogEntryId: entry.id,
|
|
50
|
+
enabled: raw['enabled'] ?? true,
|
|
51
|
+
configuration: raw['configuration'] ?? {}
|
|
52
|
+
});
|
|
53
|
+
const database = this.requireDatabase();
|
|
54
|
+
const secretEnvironment = isRecord(raw['secretEnvironment'])
|
|
55
|
+
? Object.fromEntries(Object.entries(raw['secretEnvironment']).filter((entry) => typeof entry[1] === 'string'))
|
|
56
|
+
: {};
|
|
57
|
+
const cwd = targetKind === 'WORKSPACE' ? database.getWorkspace(targetId)?.path : homedir();
|
|
58
|
+
if (cwd === undefined)
|
|
59
|
+
throw new Error('WORKSPACE_NOT_FOUND');
|
|
60
|
+
if (input.enabled)
|
|
61
|
+
await this.verifier.verify({
|
|
62
|
+
entry,
|
|
63
|
+
configuration: input.configuration,
|
|
64
|
+
secrets: secretEnvironment,
|
|
65
|
+
cwd
|
|
66
|
+
});
|
|
67
|
+
database.saveMcpInstallation({
|
|
68
|
+
targetKind,
|
|
69
|
+
...(targetKind === 'WORKSPACE' ? { targetId } : {}),
|
|
70
|
+
entry,
|
|
71
|
+
enabled: input.enabled,
|
|
72
|
+
configuration: input.configuration
|
|
73
|
+
});
|
|
74
|
+
return this.inspect(targetKind, targetId);
|
|
75
|
+
}
|
|
76
|
+
remove(targetKind, targetId, raw) {
|
|
77
|
+
if (!isRecord(raw) || typeof raw['serverName'] !== 'string')
|
|
78
|
+
throw new Error('MCP_INSTALL_INPUT_INVALID');
|
|
79
|
+
this.requireDatabase().removeMcpInstallation(targetKind, targetId, raw['serverName']);
|
|
80
|
+
return this.inspect(targetKind, targetId);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function workspaceId(raw) {
|
|
84
|
+
if (!isRecord(raw) || typeof raw['workspaceId'] !== 'string' || raw['workspaceId'] === '')
|
|
85
|
+
throw new Error('WORKSPACE_INVALID');
|
|
86
|
+
return raw['workspaceId'];
|
|
87
|
+
}
|
|
88
|
+
function isRecord(value) {
|
|
89
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
90
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { McpCatalogEntry } from '@myagentroam/protocol';
|
|
2
|
+
export declare class McpPackageInstaller {
|
|
3
|
+
private readonly dataDirectory;
|
|
4
|
+
constructor(dataDirectory: string | (() => string));
|
|
5
|
+
prepare(entry: McpCatalogEntry): Promise<McpCatalogEntry>;
|
|
6
|
+
private install;
|
|
7
|
+
private prepareRuntimeEnvironment;
|
|
8
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
3
|
+
import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { dirname, resolve } from 'node:path';
|
|
5
|
+
import { promisify } from 'node:util';
|
|
6
|
+
const execute = promisify(execFile);
|
|
7
|
+
const installs = new Map();
|
|
8
|
+
export class McpPackageInstaller {
|
|
9
|
+
dataDirectory;
|
|
10
|
+
constructor(dataDirectory) {
|
|
11
|
+
this.dataDirectory = dataDirectory;
|
|
12
|
+
}
|
|
13
|
+
prepare(entry) {
|
|
14
|
+
if (entry.runtime.transport !== 'STDIO')
|
|
15
|
+
return Promise.resolve(entry);
|
|
16
|
+
const request = packageRequest(entry.runtime.command, entry.runtime.args);
|
|
17
|
+
if (request === undefined)
|
|
18
|
+
return Promise.resolve(entry);
|
|
19
|
+
const key = `${request.spec}\0${entry.serverName}`;
|
|
20
|
+
const active = installs.get(key);
|
|
21
|
+
if (active !== undefined)
|
|
22
|
+
return active;
|
|
23
|
+
const operation = this.install(entry, request).finally(() => installs.delete(key));
|
|
24
|
+
installs.set(key, operation);
|
|
25
|
+
return operation;
|
|
26
|
+
}
|
|
27
|
+
async install(entry, request) {
|
|
28
|
+
const dataDirectory = typeof this.dataDirectory === 'string' ? this.dataDirectory : this.dataDirectory();
|
|
29
|
+
if (entry.runtime.transport !== 'STDIO')
|
|
30
|
+
return entry;
|
|
31
|
+
const runtimeEnvironment = entry.runtime.environment;
|
|
32
|
+
const digest = createHash('sha256').update(request.spec).digest('hex');
|
|
33
|
+
const root = resolve(dataDirectory, 'mcp-packages', digest);
|
|
34
|
+
const metadataPath = resolve(root, 'installation.json');
|
|
35
|
+
let executable = await installedExecutable(metadataPath, request.spec);
|
|
36
|
+
if (executable === undefined) {
|
|
37
|
+
const staging = `${root}.install-${randomUUID()}`;
|
|
38
|
+
await mkdir(dirname(root), { recursive: true, mode: 0o700 });
|
|
39
|
+
try {
|
|
40
|
+
await mkdir(staging, { recursive: true, mode: 0o700 });
|
|
41
|
+
await runNpmInstall(staging, request.spec);
|
|
42
|
+
executable = await resolvePackageExecutable(staging, request.spec);
|
|
43
|
+
await writeFile(resolve(staging, 'installation.json'), `${JSON.stringify({ spec: request.spec, executable: executable.slice(staging.length) })}\n`, { mode: 0o600 });
|
|
44
|
+
await rm(root, { recursive: true, force: true });
|
|
45
|
+
await rename(staging, root);
|
|
46
|
+
executable = resolve(root, `.${executable.slice(staging.length)}`);
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
await rm(staging, { recursive: true, force: true });
|
|
50
|
+
throw packageError(error);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const prefixArgs = await this.prepareRuntimeEnvironment(entry.serverName, root, dataDirectory);
|
|
54
|
+
return {
|
|
55
|
+
...entry,
|
|
56
|
+
runtime: {
|
|
57
|
+
transport: 'STDIO',
|
|
58
|
+
command: process.execPath,
|
|
59
|
+
args: [...prefixArgs, executable, ...request.serverArgs],
|
|
60
|
+
...(runtimeEnvironment === undefined ? {} : { environment: runtimeEnvironment })
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
async prepareRuntimeEnvironment(serverName, packageRoot, dataDirectory) {
|
|
65
|
+
if (serverName !== 'io.github.microsoft/playwright-mcp')
|
|
66
|
+
return [];
|
|
67
|
+
const browserRoot = resolve(dataDirectory, 'mcp-browsers', 'playwright');
|
|
68
|
+
await mkdir(browserRoot, { recursive: true, mode: 0o700 });
|
|
69
|
+
const preload = resolve(packageRoot, 'playwright-environment.cjs');
|
|
70
|
+
await writeFile(preload, `process.env.PLAYWRIGHT_BROWSERS_PATH = ${JSON.stringify(browserRoot)};\n`, { mode: 0o600 });
|
|
71
|
+
const playwright = resolve(packageRoot, 'node_modules', 'playwright', 'cli.js');
|
|
72
|
+
await run(process.execPath, [playwright, 'install', 'chromium'], {
|
|
73
|
+
PLAYWRIGHT_BROWSERS_PATH: browserRoot
|
|
74
|
+
});
|
|
75
|
+
const modulePath = resolve(packageRoot, 'node_modules', 'playwright');
|
|
76
|
+
await run(process.execPath, [
|
|
77
|
+
'-e',
|
|
78
|
+
`const {chromium}=require(${JSON.stringify(modulePath)});chromium.launch({headless:true}).then(b=>b.close())`
|
|
79
|
+
], { PLAYWRIGHT_BROWSERS_PATH: browserRoot });
|
|
80
|
+
return ['--require', preload];
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function packageRequest(command, args) {
|
|
84
|
+
if (command !== 'npx')
|
|
85
|
+
return undefined;
|
|
86
|
+
const index = args.findIndex((argument) => !argument.startsWith('-'));
|
|
87
|
+
const spec = index < 0 ? '' : args[index];
|
|
88
|
+
if (!/^(?:@[a-z0-9._-]+\/)?[a-z0-9._-]+@\d[0-9A-Za-z.+-]*$/u.test(spec))
|
|
89
|
+
throw new Error('MCP_PACKAGE_SPEC_INVALID');
|
|
90
|
+
return { spec, serverArgs: args.slice(index + 1) };
|
|
91
|
+
}
|
|
92
|
+
async function runNpmInstall(root, spec) {
|
|
93
|
+
await run(process.platform === 'win32' ? 'npm.cmd' : 'npm', [
|
|
94
|
+
'install',
|
|
95
|
+
'--prefix',
|
|
96
|
+
root,
|
|
97
|
+
'--no-save',
|
|
98
|
+
'--package-lock=false',
|
|
99
|
+
'--ignore-scripts',
|
|
100
|
+
spec
|
|
101
|
+
]);
|
|
102
|
+
}
|
|
103
|
+
async function run(command, args, environment = {}) {
|
|
104
|
+
await execute(command, [...args], {
|
|
105
|
+
env: { ...process.env, ...environment },
|
|
106
|
+
timeout: 300_000,
|
|
107
|
+
maxBuffer: 1024 * 1024,
|
|
108
|
+
...(process.platform === 'win32' && command.endsWith('.cmd') ? { shell: true } : {})
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
async function resolvePackageExecutable(root, spec) {
|
|
112
|
+
const packageName = spec.slice(0, spec.lastIndexOf('@'));
|
|
113
|
+
const packageRoot = resolve(root, 'node_modules', ...packageName.split('/'));
|
|
114
|
+
const manifest = JSON.parse(await readFile(resolve(packageRoot, 'package.json'), 'utf8'));
|
|
115
|
+
const candidates = typeof manifest.bin === 'string' ? [manifest.bin] : Object.values(manifest.bin ?? {});
|
|
116
|
+
if (candidates.length !== 1)
|
|
117
|
+
throw new Error('MCP_PACKAGE_EXECUTABLE_INVALID');
|
|
118
|
+
return resolve(packageRoot, candidates[0]);
|
|
119
|
+
}
|
|
120
|
+
async function installedExecutable(metadataPath, spec) {
|
|
121
|
+
try {
|
|
122
|
+
const metadata = JSON.parse(await readFile(metadataPath, 'utf8'));
|
|
123
|
+
if (metadata['spec'] !== spec || typeof metadata['executable'] !== 'string')
|
|
124
|
+
return undefined;
|
|
125
|
+
return resolve(dirname(metadataPath), `.${metadata['executable']}`);
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
function packageError(error) {
|
|
132
|
+
const message = error instanceof Error ? error.message : '';
|
|
133
|
+
if (message.startsWith('MCP_'))
|
|
134
|
+
return new Error(message);
|
|
135
|
+
return new Error('MCP_PACKAGE_INSTALL_FAILED');
|
|
136
|
+
}
|
|
@@ -33,8 +33,7 @@ export class NodeConnectionLifecycleService {
|
|
|
33
33
|
const registered = {
|
|
34
34
|
serverUrl: config.serverUrl,
|
|
35
35
|
allowedRoots: config.allowedRoots,
|
|
36
|
-
...(config.
|
|
37
|
-
...(config.upgrade === undefined ? {} : { upgrade: config.upgrade }),
|
|
36
|
+
...(config.dataDirectory === undefined ? {} : { dataDirectory: config.dataDirectory }),
|
|
38
37
|
nodeId,
|
|
39
38
|
credential
|
|
40
39
|
};
|
|
@@ -17,19 +17,21 @@ export declare class RunnerService {
|
|
|
17
17
|
id: string;
|
|
18
18
|
label: string;
|
|
19
19
|
supportedEfforts: string[];
|
|
20
|
+
isDefault?: boolean | undefined;
|
|
21
|
+
defaultEffort?: string | undefined;
|
|
20
22
|
}[];
|
|
21
23
|
accessOptions: {
|
|
22
24
|
id: string;
|
|
23
|
-
label: string;
|
|
24
25
|
description: string;
|
|
26
|
+
label: string;
|
|
25
27
|
}[];
|
|
26
28
|
commands: {
|
|
27
29
|
id: string;
|
|
30
|
+
description: string;
|
|
28
31
|
available: boolean;
|
|
29
32
|
runner: "codex" | "claude-code" | "opencode";
|
|
30
33
|
reasonCode: string | null;
|
|
31
34
|
label: string;
|
|
32
|
-
description: string;
|
|
33
35
|
inputHint: string;
|
|
34
36
|
source: "CODEX_APP_SERVER" | "CLAUDE_AGENT_SDK" | "OPENCODE_SERVER";
|
|
35
37
|
interaction: "RUNNER_TEXT" | "IMMEDIATE_ACTION" | "TOGGLE" | "VALUE";
|
|
@@ -54,14 +54,17 @@ export class RunnerService {
|
|
|
54
54
|
profiles: await this.refreshProfiles(workspaceId, environment),
|
|
55
55
|
defaults: this.database()
|
|
56
56
|
.getRunnerDefaults()
|
|
57
|
-
.filter((value) => this.runners.supportsConfiguration(value))
|
|
57
|
+
.filter((value) => this.runners.supportsConfiguration(value, undefined, environment))
|
|
58
58
|
};
|
|
59
59
|
}
|
|
60
60
|
saveDefaults(data) {
|
|
61
|
+
const environment = parseSecretEnvironment(typeof data === 'object' && data !== null
|
|
62
|
+
? data.secretEnvironment
|
|
63
|
+
: undefined);
|
|
61
64
|
const parsed = runnerDefaultConfigurationSchema.safeParse(data);
|
|
62
65
|
if (!parsed.success)
|
|
63
66
|
throw new Error('RUNNER_DEFAULT_INVALID');
|
|
64
|
-
if (!this.runners.supportsConfiguration(parsed.data))
|
|
67
|
+
if (!this.runners.supportsConfiguration(parsed.data, undefined, environment))
|
|
65
68
|
throw new Error('RUNNER_CONFIGURATION_UNSUPPORTED');
|
|
66
69
|
return { defaults: this.database().saveRunnerDefaults(parsed.data) };
|
|
67
70
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { validSessionEffort, validSessionOption, validSessionTitle } from '../util/node-operation-parsers.js';
|
|
2
2
|
import { isPlainRecord } from '../util/node-operation-parsers.js';
|
|
3
|
+
import { parseSecretEnvironment } from '../util/secret-environment.js';
|
|
3
4
|
export class SessionLifecycleService {
|
|
4
5
|
options;
|
|
5
6
|
starts = new Map();
|
|
@@ -75,18 +76,19 @@ export class SessionLifecycleService {
|
|
|
75
76
|
const workspace = database.getWorkspace(input.workspaceId);
|
|
76
77
|
if (workspace === undefined)
|
|
77
78
|
throw new Error('WORKSPACE_NOT_FOUND');
|
|
79
|
+
const environment = parseSecretEnvironment(input.secretEnvironment);
|
|
78
80
|
const saved = database
|
|
79
81
|
.getRunnerDefaults()
|
|
80
82
|
.find((value) => value.runner === runner.name &&
|
|
81
|
-
this.options.runners.supportsConfiguration(value, workspace));
|
|
82
|
-
const fallback = runner.defaultConfiguration(workspace);
|
|
83
|
+
this.options.runners.supportsConfiguration(value, workspace, environment));
|
|
84
|
+
const fallback = runner.defaultConfiguration(workspace, environment);
|
|
83
85
|
const configuration = {
|
|
84
86
|
runner: runner.name,
|
|
85
87
|
model: typeof input.model === 'string' ? input.model : (saved?.model ?? fallback.model),
|
|
86
88
|
effort: typeof input.effort === 'string' ? input.effort : (saved?.effort ?? fallback.effort),
|
|
87
89
|
access: typeof input.access === 'string' ? input.access : (saved?.access ?? fallback.access)
|
|
88
90
|
};
|
|
89
|
-
if (!this.options.runners.supportsConfiguration(configuration, workspace))
|
|
91
|
+
if (!this.options.runners.supportsConfiguration(configuration, workspace, environment))
|
|
90
92
|
throw new Error('RUNNER_CONFIGURATION_UNSUPPORTED');
|
|
91
93
|
const session = this.options.runtime.createAgentSession({
|
|
92
94
|
workspaceId: input.workspaceId,
|
|
@@ -125,8 +127,9 @@ export class SessionLifecycleService {
|
|
|
125
127
|
access: input.access
|
|
126
128
|
};
|
|
127
129
|
const workspace = this.options.database().getWorkspace(current.workspaceId);
|
|
130
|
+
const environment = parseSecretEnvironment(input.secretEnvironment);
|
|
128
131
|
if (workspace === undefined ||
|
|
129
|
-
!this.options.runners.supportsConfiguration(configuration, workspace))
|
|
132
|
+
!this.options.runners.supportsConfiguration(configuration, workspace, environment))
|
|
130
133
|
throw new Error('RUNNER_CONFIGURATION_UNSUPPORTED');
|
|
131
134
|
const session = this.options.runtime.updateAgentSessionConfiguration({
|
|
132
135
|
sessionId: current.id,
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type SkillInstallMetadata } from '@myagentroam/protocol';
|
|
2
|
+
export type LocalSkillStatus = 'VALID' | 'UNKNOWN_VERSION' | 'PARTIAL' | 'INVALID' | 'INVALID_LINK' | 'GIT_EXCLUDE_BROKEN';
|
|
3
|
+
export interface LocalSkillInspection {
|
|
4
|
+
readonly name: string;
|
|
5
|
+
readonly description: string;
|
|
6
|
+
readonly localStatus: LocalSkillStatus;
|
|
7
|
+
readonly install?: SkillInstallMetadata;
|
|
8
|
+
}
|
|
9
|
+
export interface LocalSkillTargetInspection {
|
|
10
|
+
readonly targetKind: 'NODE' | 'WORKSPACE';
|
|
11
|
+
readonly compatibilityMode: 'SYMLINK' | 'JUNCTION' | 'MANAGED_COPY' | 'UNAVAILABLE';
|
|
12
|
+
readonly skills: readonly LocalSkillInspection[];
|
|
13
|
+
}
|
|
14
|
+
export declare class SkillDirectoryService {
|
|
15
|
+
inspectNodeHome(home: string): Promise<LocalSkillTargetInspection>;
|
|
16
|
+
inspectWorkspace(workspace: string): Promise<LocalSkillTargetInspection>;
|
|
17
|
+
}
|