@myagentroam/node 0.1.8 → 0.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/capabilities.js +6 -3
- package/dist/claude-agent-sdk.d.ts +1 -0
- package/dist/claude-agent-sdk.js +8 -1
- package/dist/codex-app-server.d.ts +6 -0
- package/dist/codex-app-server.js +6 -0
- package/dist/connector/node-connector-options.d.ts +1 -0
- package/dist/connector.d.ts +2 -0
- package/dist/connector.js +43 -14
- package/dist/database.d.ts +23 -0
- package/dist/database.js +84 -2
- package/dist/migrations/v004.d.ts +4 -0
- package/dist/migrations/v004.js +35 -0
- package/dist/native-session-history.js +72 -14
- package/dist/opencode-server.d.ts +1 -0
- package/dist/opencode-server.js +12 -0
- package/dist/runner/abstract-runner.d.ts +13 -3
- package/dist/runner/abstract-runner.js +78 -4
- package/dist/runner/claude/managed-run-controller.js +18 -7
- package/dist/runner/claude-code-runner.d.ts +1 -0
- package/dist/runner/claude-code-runner.js +17 -4
- package/dist/runner/codex/conversation-parser.d.ts +2 -2
- package/dist/runner/codex/conversation-parser.js +30 -12
- package/dist/runner/codex/managed-run-controller.js +11 -8
- package/dist/runner/codex-runner.d.ts +17 -3
- package/dist/runner/codex-runner.js +225 -12
- package/dist/runner/opencode/conversation-parser.d.ts +11 -7
- package/dist/runner/opencode/conversation-parser.js +15 -15
- package/dist/runner/opencode/managed-run-controller.d.ts +2 -1
- package/dist/runner/opencode/managed-run-controller.js +36 -12
- 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-command-engine.js +1 -1
- package/dist/runner-profiles.js +29 -41
- package/dist/runner-usage.js +5 -5
- package/dist/runtime-command-detector.d.ts +3 -0
- package/dist/runtime-command-detector.js +78 -0
- package/dist/service/conversation-history-service.js +55 -24
- package/dist/service/conversation-segment-service.d.ts +20 -0
- package/dist/service/conversation-segment-service.js +155 -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/native-session-watch-service.d.ts +1 -0
- package/dist/service/native-session-watch-service.js +21 -4
- package/dist/service/node-request-service.js +2 -1
- package/dist/service/run-attachment-service.d.ts +3 -2
- package/dist/service/run-attachment-service.js +21 -11
- package/dist/service/run-event-service.d.ts +1 -0
- package/dist/service/run-event-service.js +8 -2
- package/dist/service/runner-service.d.ts +2 -0
- package/dist/service/runner-service.js +5 -2
- package/dist/service/session-catalog-service.js +4 -4
- package/dist/service/session-lifecycle-service.js +7 -4
- package/dist/service/session-presentation-service.d.ts +1 -0
- package/dist/service/session-presentation-service.js +8 -1
- package/dist/service/session-query-service.d.ts +4 -0
- package/dist/service/session-query-service.js +6 -1
- package/dist/service/skill-directory-service.js +3 -26
- package/dist/service/skill-install-service.js +4 -74
- package/dist/service/skill-node-operation-service.js +2 -1
- package/dist/service/workbench-manifest-service.d.ts +2 -0
- package/dist/service/workspace-git-exclude-service.d.ts +4 -0
- package/dist/service/workspace-git-exclude-service.js +119 -0
- package/dist/service/workspace-queue-workbench-service.d.ts +2 -0
- package/dist/service/workspace-queue-workbench-service.js +5 -3
- package/dist/service/workspace-service.js +2 -0
- package/dist/supervisor.js +2 -1
- package/dist/terminal.js +1 -1
- package/dist/util/node-operation-parsers.js +2 -2
- package/dist/util/runner-native-session-parsers.d.ts +2 -3
- package/dist/util/runner-native-session-parsers.js +5 -14
- package/dist/util/safe-error.d.ts +2 -0
- package/dist/util/safe-error.js +5 -0
- package/package.json +2 -2
|
@@ -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.1' });
|
|
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
|
+
}
|
|
@@ -8,6 +8,7 @@ export interface NativeSessionWatchOptions<TPage extends {
|
|
|
8
8
|
readonly refreshActivity: (session: NodeAgentSession) => Promise<void>;
|
|
9
9
|
readonly present: (session: NodeAgentSession) => unknown;
|
|
10
10
|
readonly readPage: (session: NodeAgentSession, limit: number) => Promise<TPage>;
|
|
11
|
+
readonly projectInitialPage?: (session: NodeAgentSession, page: TPage, unit: 'SEGMENT', limit: number) => unknown;
|
|
11
12
|
readonly emitSession: (session: unknown) => void;
|
|
12
13
|
readonly emitTurn: (turn: unknown) => void;
|
|
13
14
|
}
|
|
@@ -15,7 +15,12 @@ export class NativeSessionWatchService {
|
|
|
15
15
|
};
|
|
16
16
|
}
|
|
17
17
|
async executeWatch(input) {
|
|
18
|
-
|
|
18
|
+
const projectInitialPage = this.options.projectInitialPage;
|
|
19
|
+
if (input.unit === 'SEGMENT' && projectInitialPage === undefined)
|
|
20
|
+
throw new Error('SESSION_INVALID');
|
|
21
|
+
if (typeof input.sessionId !== 'string' ||
|
|
22
|
+
(input.mode !== 'initial' && input.mode !== 'renew') ||
|
|
23
|
+
(input.unit !== undefined && input.unit !== 'SEGMENT'))
|
|
19
24
|
throw new Error('SESSION_INVALID');
|
|
20
25
|
const session = await this.options.resolve(input.sessionId);
|
|
21
26
|
if (session === undefined || session.externalSessionId === null)
|
|
@@ -27,9 +32,21 @@ export class NativeSessionWatchService {
|
|
|
27
32
|
return { renewed: true };
|
|
28
33
|
}
|
|
29
34
|
const watch = this.watch(session);
|
|
30
|
-
const
|
|
31
|
-
if (watch !== undefined &&
|
|
35
|
+
const turnSnapshot = watch === undefined ? undefined : await this.refresh(session.id, INITIAL_TURN_LIMIT);
|
|
36
|
+
if (watch !== undefined && turnSnapshot === undefined) {
|
|
37
|
+
this.stop(session.id);
|
|
32
38
|
throw new Error('NATIVE_TRANSCRIPT_UNAVAILABLE');
|
|
39
|
+
}
|
|
40
|
+
let snapshot = turnSnapshot;
|
|
41
|
+
if (turnSnapshot !== undefined && input.unit === 'SEGMENT') {
|
|
42
|
+
try {
|
|
43
|
+
snapshot = projectInitialPage(session, turnSnapshot, 'SEGMENT', 10);
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
this.stop(session.id);
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
33
50
|
return {
|
|
34
51
|
session: this.options.present(session),
|
|
35
52
|
watch: watch === undefined ? null : { expiresAt: watch.expiresAt },
|
|
@@ -131,7 +148,7 @@ export class NativeSessionWatchService {
|
|
|
131
148
|
}
|
|
132
149
|
const page = await this.options.readPage(session, limit);
|
|
133
150
|
watch.failures = 0;
|
|
134
|
-
const turns = page.turns.length <= 10 ? page.turns : page.turns.slice(
|
|
151
|
+
const turns = page.turns.length <= 10 ? page.turns : page.turns.slice(-10);
|
|
135
152
|
const signature = JSON.stringify(turns);
|
|
136
153
|
if (signature === watch.signature)
|
|
137
154
|
return page;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { nodeLog } from '../operational.js';
|
|
2
2
|
import { validUserAuthorization } from '../runner/codex/conversation-parser.js';
|
|
3
|
+
import { safeErrorCode } from '../util/safe-error.js';
|
|
3
4
|
/** Validates and executes the closed Admin-facing Node operation envelope. */
|
|
4
5
|
export class NodeRequestService {
|
|
5
6
|
options;
|
|
@@ -44,7 +45,7 @@ export class NodeRequestService {
|
|
|
44
45
|
durationMs: Math.round(performance.now() - startedAt),
|
|
45
46
|
error: error instanceof Error ? error.message : 'INTERNAL_ERROR'
|
|
46
47
|
});
|
|
47
|
-
this.options.send('node.response', { error: error
|
|
48
|
+
this.options.send('node.response', { error: safeErrorCode(error, 'INTERNAL_ERROR') }, envelope.id);
|
|
48
49
|
}
|
|
49
50
|
}
|
|
50
51
|
}
|
|
@@ -11,14 +11,15 @@ export interface NativeImageSource {
|
|
|
11
11
|
readonly name: string;
|
|
12
12
|
readonly mime: string;
|
|
13
13
|
readonly path: string;
|
|
14
|
+
readonly root: string;
|
|
14
15
|
}
|
|
15
16
|
export type RunnerImageAttachment = NodeMessageAttachmentInput & {
|
|
16
17
|
readonly mime: 'image/png' | 'image/jpeg' | 'image/gif' | 'image/webp';
|
|
17
18
|
};
|
|
18
19
|
export declare class RunAttachmentService {
|
|
19
|
-
private readonly
|
|
20
|
+
private readonly nativeImageRoots;
|
|
20
21
|
readonly state: AttachmentDomainState<CachedRunImages, NativeImageSource>;
|
|
21
|
-
constructor(
|
|
22
|
+
constructor(nativeImageRoots: (session: NodeAgentSession) => readonly string[]);
|
|
22
23
|
materializeCodex(runId: string, attachments: readonly RunnerImageAttachment[]): Promise<readonly Extract<CodexComposerInput, {
|
|
23
24
|
readonly type: 'localImage';
|
|
24
25
|
}>[]>;
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { tmpdir } from 'node:os';
|
|
2
2
|
import { isAbsolute, join, relative, resolve } from 'node:path';
|
|
3
|
-
import { lstat, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { lstat, mkdir, mkdtemp, readFile, realpath, rm, writeFile } from 'node:fs/promises';
|
|
4
4
|
import { AttachmentDomainState } from './attachment-domain-state.js';
|
|
5
5
|
const MAX_ATTACHMENT_BYTES = 5 * 1024 * 1024;
|
|
6
6
|
const IMAGE_CACHE_TTL_MS = 60 * 60_000;
|
|
7
7
|
export class RunAttachmentService {
|
|
8
|
-
|
|
8
|
+
nativeImageRoots;
|
|
9
9
|
state = new AttachmentDomainState();
|
|
10
|
-
constructor(
|
|
11
|
-
this.
|
|
10
|
+
constructor(nativeImageRoots) {
|
|
11
|
+
this.nativeImageRoots = nativeImageRoots;
|
|
12
12
|
}
|
|
13
13
|
async materializeCodex(runId, attachments) {
|
|
14
14
|
if (attachments.length === 0)
|
|
@@ -95,8 +95,8 @@ export class RunAttachmentService {
|
|
|
95
95
|
return cached.images[imageIndex];
|
|
96
96
|
}
|
|
97
97
|
registerNative(session, images) {
|
|
98
|
-
const
|
|
99
|
-
if (
|
|
98
|
+
const roots = this.nativeImageRoots(session).map((root) => resolve(root));
|
|
99
|
+
if (roots.length === 0)
|
|
100
100
|
return [];
|
|
101
101
|
const runId = `native-history:${session.id}`;
|
|
102
102
|
const sources = this.state.nativeHistoryImages.get(runId) ?? [];
|
|
@@ -107,15 +107,18 @@ export class RunAttachmentService {
|
|
|
107
107
|
continue;
|
|
108
108
|
}
|
|
109
109
|
const path = resolve(image.localPath);
|
|
110
|
-
const
|
|
111
|
-
|
|
110
|
+
const root = roots.find((candidate) => {
|
|
111
|
+
const relation = relative(candidate, path);
|
|
112
|
+
return relation.length > 0 && !relation.startsWith('..') && !isAbsolute(relation);
|
|
113
|
+
});
|
|
114
|
+
if (root === undefined) {
|
|
112
115
|
attachments.push({ name: image.name, mime: image.mime });
|
|
113
116
|
continue;
|
|
114
117
|
}
|
|
115
118
|
let imageIndex = sources.findIndex((source) => source.path === path);
|
|
116
119
|
if (imageIndex < 0) {
|
|
117
120
|
imageIndex = sources.length;
|
|
118
|
-
sources.push({ name: image.name, mime: image.mime, path });
|
|
121
|
+
sources.push({ name: image.name, mime: image.mime, path, root });
|
|
119
122
|
}
|
|
120
123
|
attachments.push({ name: image.name, mime: image.mime, runId, imageIndex });
|
|
121
124
|
}
|
|
@@ -128,10 +131,17 @@ export class RunAttachmentService {
|
|
|
128
131
|
if (source === undefined)
|
|
129
132
|
return undefined;
|
|
130
133
|
try {
|
|
131
|
-
const
|
|
134
|
+
const [actualRoot, actualPath] = await Promise.all([
|
|
135
|
+
realpath(source.root),
|
|
136
|
+
realpath(source.path)
|
|
137
|
+
]);
|
|
138
|
+
const relation = relative(actualRoot, actualPath);
|
|
139
|
+
if (relation.length === 0 || relation.startsWith('..') || isAbsolute(relation))
|
|
140
|
+
return undefined;
|
|
141
|
+
const info = await lstat(actualPath);
|
|
132
142
|
if (!info.isFile() || info.size <= 0 || info.size > MAX_ATTACHMENT_BYTES)
|
|
133
143
|
return undefined;
|
|
134
|
-
return { ...source, dataBase64: (await readFile(
|
|
144
|
+
return { ...source, dataBase64: (await readFile(actualPath)).toString('base64') };
|
|
135
145
|
}
|
|
136
146
|
catch {
|
|
137
147
|
return undefined;
|
|
@@ -25,6 +25,7 @@ export declare class RunEventService {
|
|
|
25
25
|
constructor(options: RunEventServiceOptions);
|
|
26
26
|
emit(runId: string, eventType: string, payload: unknown, status?: FakeRunnerStatus): void;
|
|
27
27
|
clear(): void;
|
|
28
|
+
flushPendingText(runId: string): void;
|
|
28
29
|
private enqueueText;
|
|
29
30
|
private flushText;
|
|
30
31
|
private emitNow;
|
|
@@ -25,6 +25,9 @@ export class RunEventService {
|
|
|
25
25
|
clearTimeout(pending.timer);
|
|
26
26
|
this.pendingText.clear();
|
|
27
27
|
}
|
|
28
|
+
flushPendingText(runId) {
|
|
29
|
+
this.flushText(runId);
|
|
30
|
+
}
|
|
28
31
|
enqueueText(runId, text) {
|
|
29
32
|
if (text.length === 0)
|
|
30
33
|
return;
|
|
@@ -59,8 +62,9 @@ export class RunEventService {
|
|
|
59
62
|
nodeLog('run.event', { runId, eventType, status });
|
|
60
63
|
if (status === 'FAILED')
|
|
61
64
|
this.options.metrics.runnerFailed();
|
|
65
|
+
let persisted = undefined;
|
|
62
66
|
try {
|
|
63
|
-
this.options.runtime.appendRunEvent({
|
|
67
|
+
persisted = this.options.runtime.appendRunEvent({
|
|
64
68
|
runId,
|
|
65
69
|
sequence: event.sequence,
|
|
66
70
|
eventType,
|
|
@@ -84,7 +88,9 @@ export class RunEventService {
|
|
|
84
88
|
if (session !== undefined)
|
|
85
89
|
this.options.emitWorkbench('session', { session: this.options.presentSession(session) });
|
|
86
90
|
}
|
|
87
|
-
this.options.emitWorkbench('conversation', {
|
|
91
|
+
this.options.emitWorkbench('conversation', {
|
|
92
|
+
event: { ...event, createdAt: persisted?.createdAt ?? Date.now() }
|
|
93
|
+
});
|
|
88
94
|
if (status === undefined || !isTerminalRunStatus(status))
|
|
89
95
|
return;
|
|
90
96
|
const turn = this.options.runtime.conversationTurnForRun(runId);
|
|
@@ -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 { removeNativeSession } from '../native-session-history.js';
|
|
2
2
|
import { isTerminalRunStatus, sessionPage } from '../util/node-operation-parsers.js';
|
|
3
|
+
import { latestSessionActivityAt } from './session-presentation-service.js';
|
|
3
4
|
export class SessionCatalogService {
|
|
4
5
|
options;
|
|
5
6
|
rewindQueue = Promise.resolve();
|
|
@@ -35,7 +36,7 @@ export class SessionCatalogService {
|
|
|
35
36
|
pinOrder: source.pinOrder ?? null,
|
|
36
37
|
externalDiscovered: true,
|
|
37
38
|
titleSource: session.customTitle === null ? source.titleSource : session.titleSource,
|
|
38
|
-
lastActivityAt: source.lastActivityAt
|
|
39
|
+
lastActivityAt: latestSessionActivityAt(session.lastActivityAt, source.lastActivityAt)
|
|
39
40
|
});
|
|
40
41
|
});
|
|
41
42
|
const merged = new Map([
|
|
@@ -100,8 +101,7 @@ export class SessionCatalogService {
|
|
|
100
101
|
delivery: resumed === undefined ? 'REJECTED' : 'RESUMABLE',
|
|
101
102
|
...(resumed === undefined
|
|
102
103
|
? {
|
|
103
|
-
reasonCode: failure?.code ?? 'SESSION_RESUME_REJECTED'
|
|
104
|
-
...(failure?.detail === undefined ? {} : { reasonDetail: failure.detail })
|
|
104
|
+
reasonCode: failure?.code ?? 'SESSION_RESUME_REJECTED'
|
|
105
105
|
}
|
|
106
106
|
: {})
|
|
107
107
|
};
|
|
@@ -130,7 +130,7 @@ export class SessionCatalogService {
|
|
|
130
130
|
const expected = input.position === 'BEFORE' ? turn?.rewind?.before : turn?.rewind?.after;
|
|
131
131
|
if (turn === undefined || expected !== input.boundary)
|
|
132
132
|
throw new Error('SESSION_REWIND_TARGET_INVALID');
|
|
133
|
-
const title = rewindTitle(source.customTitle ?? source.runnerTitle ?? '
|
|
133
|
+
const title = rewindTitle(source.customTitle ?? source.runnerTitle ?? 'Untitled session', this.options.runtime
|
|
134
134
|
.listAgentSessions()
|
|
135
135
|
.flatMap((candidate) => [candidate.customTitle ?? candidate.runnerTitle].filter(isString)));
|
|
136
136
|
const runner = this.options.runners.require(source.runner);
|
|
@@ -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,
|
|
@@ -30,4 +30,5 @@ export declare class SessionPresentationService {
|
|
|
30
30
|
present(session: NodeAgentSession): PresentedSession;
|
|
31
31
|
presentDiscovered(sessions: readonly NodeAgentSession[]): readonly PresentedSession[];
|
|
32
32
|
}
|
|
33
|
+
export declare function latestSessionActivityAt(current: number, discovered: number): number;
|
|
33
34
|
export {};
|