@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.
Files changed (79) hide show
  1. package/dist/capabilities.js +6 -3
  2. package/dist/claude-agent-sdk.d.ts +1 -0
  3. package/dist/claude-agent-sdk.js +8 -1
  4. package/dist/codex-app-server.d.ts +6 -0
  5. package/dist/codex-app-server.js +6 -0
  6. package/dist/connector/node-connector-options.d.ts +1 -0
  7. package/dist/connector.d.ts +2 -0
  8. package/dist/connector.js +43 -14
  9. package/dist/database.d.ts +23 -0
  10. package/dist/database.js +84 -2
  11. package/dist/migrations/v004.d.ts +4 -0
  12. package/dist/migrations/v004.js +35 -0
  13. package/dist/native-session-history.js +72 -14
  14. package/dist/opencode-server.d.ts +1 -0
  15. package/dist/opencode-server.js +12 -0
  16. package/dist/runner/abstract-runner.d.ts +13 -3
  17. package/dist/runner/abstract-runner.js +78 -4
  18. package/dist/runner/claude/managed-run-controller.js +18 -7
  19. package/dist/runner/claude-code-runner.d.ts +1 -0
  20. package/dist/runner/claude-code-runner.js +17 -4
  21. package/dist/runner/codex/conversation-parser.d.ts +2 -2
  22. package/dist/runner/codex/conversation-parser.js +30 -12
  23. package/dist/runner/codex/managed-run-controller.js +11 -8
  24. package/dist/runner/codex-runner.d.ts +17 -3
  25. package/dist/runner/codex-runner.js +225 -12
  26. package/dist/runner/opencode/conversation-parser.d.ts +11 -7
  27. package/dist/runner/opencode/conversation-parser.js +15 -15
  28. package/dist/runner/opencode/managed-run-controller.d.ts +2 -1
  29. package/dist/runner/opencode/managed-run-controller.js +36 -12
  30. package/dist/runner/opencode-runner.d.ts +2 -1
  31. package/dist/runner/opencode-runner.js +23 -3
  32. package/dist/runner/runner-registry.d.ts +1 -1
  33. package/dist/runner/runner-registry.js +2 -2
  34. package/dist/runner-command-engine.js +1 -1
  35. package/dist/runner-profiles.js +29 -41
  36. package/dist/runner-usage.js +5 -5
  37. package/dist/runtime-command-detector.d.ts +3 -0
  38. package/dist/runtime-command-detector.js +78 -0
  39. package/dist/service/conversation-history-service.js +55 -24
  40. package/dist/service/conversation-segment-service.d.ts +20 -0
  41. package/dist/service/conversation-segment-service.js +155 -0
  42. package/dist/service/mcp-installation-verifier.d.ts +9 -0
  43. package/dist/service/mcp-installation-verifier.js +87 -0
  44. package/dist/service/mcp-node-operation-service.d.ts +11 -0
  45. package/dist/service/mcp-node-operation-service.js +90 -0
  46. package/dist/service/mcp-package-installer.d.ts +8 -0
  47. package/dist/service/mcp-package-installer.js +136 -0
  48. package/dist/service/native-session-watch-service.d.ts +1 -0
  49. package/dist/service/native-session-watch-service.js +21 -4
  50. package/dist/service/node-request-service.js +2 -1
  51. package/dist/service/run-attachment-service.d.ts +3 -2
  52. package/dist/service/run-attachment-service.js +21 -11
  53. package/dist/service/run-event-service.d.ts +1 -0
  54. package/dist/service/run-event-service.js +8 -2
  55. package/dist/service/runner-service.d.ts +2 -0
  56. package/dist/service/runner-service.js +5 -2
  57. package/dist/service/session-catalog-service.js +4 -4
  58. package/dist/service/session-lifecycle-service.js +7 -4
  59. package/dist/service/session-presentation-service.d.ts +1 -0
  60. package/dist/service/session-presentation-service.js +8 -1
  61. package/dist/service/session-query-service.d.ts +4 -0
  62. package/dist/service/session-query-service.js +6 -1
  63. package/dist/service/skill-directory-service.js +3 -26
  64. package/dist/service/skill-install-service.js +4 -74
  65. package/dist/service/skill-node-operation-service.js +2 -1
  66. package/dist/service/workbench-manifest-service.d.ts +2 -0
  67. package/dist/service/workspace-git-exclude-service.d.ts +4 -0
  68. package/dist/service/workspace-git-exclude-service.js +119 -0
  69. package/dist/service/workspace-queue-workbench-service.d.ts +2 -0
  70. package/dist/service/workspace-queue-workbench-service.js +5 -3
  71. package/dist/service/workspace-service.js +2 -0
  72. package/dist/supervisor.js +2 -1
  73. package/dist/terminal.js +1 -1
  74. package/dist/util/node-operation-parsers.js +2 -2
  75. package/dist/util/runner-native-session-parsers.d.ts +2 -3
  76. package/dist/util/runner-native-session-parsers.js +5 -14
  77. package/dist/util/safe-error.d.ts +2 -0
  78. package/dist/util/safe-error.js +5 -0
  79. 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
- if (typeof input.sessionId !== 'string' || (input.mode !== 'initial' && input.mode !== 'renew'))
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 snapshot = watch === undefined ? undefined : await this.refresh(session.id, INITIAL_TURN_LIMIT);
31
- if (watch !== undefined && snapshot === 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(0, 10);
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 instanceof Error ? error.message : 'INTERNAL_ERROR' }, envelope.id);
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 nativeImageRoot;
20
+ private readonly nativeImageRoots;
20
21
  readonly state: AttachmentDomainState<CachedRunImages, NativeImageSource>;
21
- constructor(nativeImageRoot: (session: NodeAgentSession) => string | undefined);
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
- nativeImageRoot;
8
+ nativeImageRoots;
9
9
  state = new AttachmentDomainState();
10
- constructor(nativeImageRoot) {
11
- this.nativeImageRoot = nativeImageRoot;
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 root = this.nativeImageRoot(session);
99
- if (root === undefined)
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 relation = relative(root, path);
111
- if (relation.length === 0 || relation.startsWith('..') || isAbsolute(relation)) {
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 info = await lstat(source.path);
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(source.path)).toString('base64') };
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', { event });
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);
@@ -17,6 +17,8 @@ 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;
@@ -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 ?? '未命名会话', this.options.runtime
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 {};