chainlesschain 0.45.64 → 0.45.65

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.
@@ -15,12 +15,28 @@ import { startChatRepl } from "../gateways/repl/chat-repl.js";
15
15
  import { ChainlessChainWSServer } from "../gateways/ws/ws-server.js";
16
16
  import { WSSessionManager } from "../gateways/ws/ws-session-gateway.js";
17
17
  import { createWebUIServer } from "../gateways/ui/web-ui-server.js";
18
- import {
19
- findProjectRoot,
20
- loadProjectConfig,
21
- } from "../lib/project-detector.js";
18
+ import { MCPClient, MCPServerConfig } from "../lib/mcp-client.js";
19
+ import sharedManagedToolPolicy from "./coding-agent-managed-tool-policy.cjs";
20
+ import { findProjectRoot, loadProjectConfig } from "../lib/project-detector.js";
22
21
  import { loadConfig } from "../lib/config-manager.js";
23
22
 
23
+ const {
24
+ DEFAULT_ALLOWED_MCP_SERVER_NAMES,
25
+ createTrustedMcpServerMap,
26
+ resolveMcpServerPolicy,
27
+ } = sharedManagedToolPolicy;
28
+
29
+ const BUILTIN_CODING_AGENT_MCP_REGISTRY = Object.freeze({
30
+ trustedServers: [
31
+ {
32
+ id: "weather",
33
+ securityLevel: "low",
34
+ requiredPermissions: ["network:http"],
35
+ capabilities: ["tools", "resources"],
36
+ },
37
+ ],
38
+ });
39
+
24
40
  function openBrowser(url) {
25
41
  try {
26
42
  const platform = process.platform;
@@ -48,11 +64,15 @@ export class AgentRuntime {
48
64
  startChatRepl: deps.startChatRepl || startChatRepl,
49
65
  bootstrap: deps.bootstrap || bootstrap,
50
66
  createServer:
51
- deps.createServer ||
52
- ((options) => new ChainlessChainWSServer(options)),
67
+ deps.createServer || ((options) => new ChainlessChainWSServer(options)),
53
68
  createSessionManager:
54
69
  deps.createSessionManager ||
55
70
  ((options) => new WSSessionManager(options)),
71
+ createMcpClient: deps.createMcpClient || (() => new MCPClient()),
72
+ createMcpServerConfig:
73
+ deps.createMcpServerConfig || ((db) => new MCPServerConfig(db)),
74
+ mcpServerRegistry:
75
+ deps.mcpServerRegistry || BUILTIN_CODING_AGENT_MCP_REGISTRY,
56
76
  createWebServer:
57
77
  deps.createWebServer || ((options) => createWebUIServer(options)),
58
78
  findProjectRoot: deps.findProjectRoot || findProjectRoot,
@@ -105,22 +125,29 @@ export class AgentRuntime {
105
125
  return this.startAgentSession();
106
126
  }
107
127
 
108
- throw new Error(`resumeSession is not supported for runtime kind "${this.kind}".`);
128
+ throw new Error(
129
+ `resumeSession is not supported for runtime kind "${this.kind}".`,
130
+ );
109
131
  }
110
132
 
111
133
  async runTurn(input, meta = {}) {
112
134
  if (typeof this.deps.runTurn !== "function") {
113
- throw new Error(`runTurn is not configured for runtime kind "${this.kind}".`);
135
+ throw new Error(
136
+ `runTurn is not configured for runtime kind "${this.kind}".`,
137
+ );
114
138
  }
115
139
 
116
140
  const startedAt = Date.now();
117
- this.emit(RUNTIME_EVENTS.TURN_START, createAgentTurnRecord({
118
- kind: this.kind,
119
- input,
120
- meta,
121
- sessionId: this.policy.sessionId || null,
122
- startedAt,
123
- }));
141
+ this.emit(
142
+ RUNTIME_EVENTS.TURN_START,
143
+ createAgentTurnRecord({
144
+ kind: this.kind,
145
+ input,
146
+ meta,
147
+ sessionId: this.policy.sessionId || null,
148
+ startedAt,
149
+ }),
150
+ );
124
151
 
125
152
  const result = await this.deps.runTurn({
126
153
  input,
@@ -130,15 +157,18 @@ export class AgentRuntime {
130
157
  context: this.context,
131
158
  });
132
159
 
133
- this.emit(RUNTIME_EVENTS.TURN_END, createAgentTurnRecord({
134
- kind: this.kind,
135
- input,
136
- meta,
137
- result,
138
- sessionId: this.policy.sessionId || null,
139
- startedAt,
140
- endedAt: Date.now(),
141
- }));
160
+ this.emit(
161
+ RUNTIME_EVENTS.TURN_END,
162
+ createAgentTurnRecord({
163
+ kind: this.kind,
164
+ input,
165
+ meta,
166
+ result,
167
+ sessionId: this.policy.sessionId || null,
168
+ startedAt,
169
+ endedAt: Date.now(),
170
+ }),
171
+ );
142
172
 
143
173
  return result;
144
174
  }
@@ -169,14 +199,8 @@ export class AgentRuntime {
169
199
 
170
200
  async startServer() {
171
201
  const { logger: runtimeLogger } = this.deps;
172
- const {
173
- port,
174
- maxConnections,
175
- timeout,
176
- token,
177
- allowRemote,
178
- project,
179
- } = this.policy;
202
+ const { port, maxConnections, timeout, token, allowRemote, project } =
203
+ this.policy;
180
204
  let { host } = this.policy;
181
205
 
182
206
  if (Number.isNaN(port) || port < 1 || port > 65535) {
@@ -191,9 +215,11 @@ export class AgentRuntime {
191
215
  }
192
216
 
193
217
  let db = null;
218
+ let rawDb = null;
194
219
  try {
195
220
  const ctx = await this.deps.bootstrap({ skipDb: false });
196
- db = ctx.db?.getDb?.() || null;
221
+ rawDb = ctx.db?.getDatabase?.() || ctx.db?.getDb?.() || null;
222
+ db = rawDb;
197
223
  } catch (_err) {
198
224
  runtimeLogger.log(
199
225
  chalk.yellow(
@@ -202,9 +228,16 @@ export class AgentRuntime {
202
228
  );
203
229
  }
204
230
 
231
+ const mcpClient = await this._initializeCodingAgentMcpClient(rawDb, {
232
+ logger: runtimeLogger,
233
+ });
234
+
205
235
  const sessionManager = this.deps.createSessionManager({
206
236
  db,
207
237
  defaultProjectRoot: project,
238
+ mcpClient,
239
+ allowedMcpServerNames: DEFAULT_ALLOWED_MCP_SERVER_NAMES,
240
+ mcpServerRegistry: this.deps.mcpServerRegistry,
208
241
  });
209
242
 
210
243
  const server = this.deps.createServer({
@@ -217,7 +250,9 @@ export class AgentRuntime {
217
250
  });
218
251
 
219
252
  server.on("connection", ({ clientId, ip }) => {
220
- runtimeLogger.log(chalk.green(` + Client connected: ${clientId} (${ip})`));
253
+ runtimeLogger.log(
254
+ chalk.green(` + Client connected: ${clientId} (${ip})`),
255
+ );
221
256
  });
222
257
 
223
258
  server.on("disconnection", ({ clientId, reason }) => {
@@ -250,6 +285,9 @@ export class AgentRuntime {
250
285
  runtimeLogger.log(
251
286
  "\n" + chalk.yellow("Shutting down WebSocket server..."),
252
287
  );
288
+ if (mcpClient && typeof mcpClient.disconnectAll === "function") {
289
+ await mcpClient.disconnectAll().catch(() => undefined);
290
+ }
253
291
  await server.stop();
254
292
  process.exit(0);
255
293
  };
@@ -305,14 +343,15 @@ export class AgentRuntime {
305
343
  ? this.deps.loadProjectConfig(projectRoot)
306
344
  : null;
307
345
  const projectName =
308
- projectConfig?.name ||
309
- (projectRoot ? path.basename(projectRoot) : null);
346
+ projectConfig?.name || (projectRoot ? path.basename(projectRoot) : null);
310
347
  const mode = projectRoot ? "project" : "global";
311
348
 
312
349
  let db = null;
350
+ let rawDb = null;
313
351
  try {
314
352
  const ctx = await this.deps.bootstrap({ skipDb: false });
315
- db = ctx.db?.getDb?.() || null;
353
+ rawDb = ctx.db?.getDatabase?.() || ctx.db?.getDb?.() || null;
354
+ db = rawDb;
316
355
  } catch (_err) {
317
356
  runtimeLogger.log(
318
357
  chalk.yellow(
@@ -322,10 +361,16 @@ export class AgentRuntime {
322
361
  }
323
362
 
324
363
  const appConfig = this.deps.loadConfig();
364
+ const mcpClient = await this._initializeCodingAgentMcpClient(rawDb, {
365
+ logger: runtimeLogger,
366
+ });
325
367
  const sessionManager = this.deps.createSessionManager({
326
368
  db,
327
369
  defaultProjectRoot: projectRoot || process.cwd(),
328
370
  config: appConfig,
371
+ mcpClient,
372
+ allowedMcpServerNames: DEFAULT_ALLOWED_MCP_SERVER_NAMES,
373
+ mcpServerRegistry: this.deps.mcpServerRegistry,
329
374
  });
330
375
 
331
376
  const wsServer = this.deps.createServer({
@@ -395,6 +440,9 @@ export class AgentRuntime {
395
440
 
396
441
  const shutdown = async () => {
397
442
  runtimeLogger.log("\n" + chalk.yellow("Shutting down UI server..."));
443
+ if (mcpClient && typeof mcpClient.disconnectAll === "function") {
444
+ await mcpClient.disconnectAll().catch(() => undefined);
445
+ }
398
446
  await Promise.all([
399
447
  new Promise((resolve) => httpServer.close(resolve)),
400
448
  wsServer.stop(),
@@ -414,4 +462,59 @@ export class AgentRuntime {
414
462
  projectName,
415
463
  };
416
464
  }
465
+
466
+ async _initializeCodingAgentMcpClient(db, options = {}) {
467
+ if (!db) {
468
+ return null;
469
+ }
470
+
471
+ const trustedMcpServers = createTrustedMcpServerMap(
472
+ this.deps.mcpServerRegistry,
473
+ );
474
+ const configStore = this.deps.createMcpServerConfig(db);
475
+ const autoConnectServers =
476
+ typeof configStore?.getAutoConnect === "function"
477
+ ? configStore.getAutoConnect()
478
+ : [];
479
+ const eligibleServers = autoConnectServers.filter(
480
+ (server) =>
481
+ resolveMcpServerPolicy(
482
+ server?.name,
483
+ { state: "connected" },
484
+ {
485
+ allowedMcpServerNames: DEFAULT_ALLOWED_MCP_SERVER_NAMES,
486
+ trustedMcpServers,
487
+ },
488
+ ).allowed,
489
+ );
490
+
491
+ if (eligibleServers.length === 0) {
492
+ return null;
493
+ }
494
+
495
+ const mcpClient = this.deps.createMcpClient();
496
+ let connectedCount = 0;
497
+
498
+ for (const server of eligibleServers) {
499
+ try {
500
+ await mcpClient.connect(server.name, server);
501
+ connectedCount += 1;
502
+ } catch (err) {
503
+ options.logger?.log?.(
504
+ chalk.yellow(
505
+ ` Warning: MCP server "${server.name}" auto-connect failed: ${err.message}`,
506
+ ),
507
+ );
508
+ }
509
+ }
510
+
511
+ if (connectedCount === 0) {
512
+ if (typeof mcpClient.disconnectAll === "function") {
513
+ await mcpClient.disconnectAll().catch(() => undefined);
514
+ }
515
+ return null;
516
+ }
517
+
518
+ return mcpClient;
519
+ }
417
520
  }
@@ -0,0 +1,294 @@
1
+ import { ToolRegistry, createDefaultToolRegistry } from "../tools/registry.js";
2
+ import sharedCodingAgentPolicy from "./coding-agent-policy.cjs";
3
+
4
+ const { TOOL_POLICY_METADATA } = sharedCodingAgentPolicy;
5
+
6
+ const CODING_AGENT_TOOL_CONTRACTS = Object.freeze([
7
+ {
8
+ name: "read_file",
9
+ title: "Read File",
10
+ kind: "filesystem",
11
+ tier: "mvp",
12
+ ...TOOL_POLICY_METADATA.read_file,
13
+ permissions: {
14
+ level: "readonly",
15
+ scopes: ["filesystem:read"],
16
+ },
17
+ telemetry: {
18
+ category: "filesystem",
19
+ tags: ["tool:read_file", "contract:coding-agent", "tier:mvp"],
20
+ },
21
+ },
22
+ {
23
+ name: "write_file",
24
+ title: "Write File",
25
+ kind: "filesystem",
26
+ tier: "mvp",
27
+ ...TOOL_POLICY_METADATA.write_file,
28
+ permissions: {
29
+ level: "elevated",
30
+ scopes: ["filesystem:write"],
31
+ },
32
+ telemetry: {
33
+ category: "filesystem",
34
+ tags: ["tool:write_file", "contract:coding-agent", "tier:mvp"],
35
+ },
36
+ },
37
+ {
38
+ name: "edit_file",
39
+ title: "Edit File",
40
+ kind: "filesystem",
41
+ tier: "mvp",
42
+ ...TOOL_POLICY_METADATA.edit_file,
43
+ permissions: {
44
+ level: "elevated",
45
+ scopes: ["filesystem:write"],
46
+ },
47
+ telemetry: {
48
+ category: "filesystem",
49
+ tags: ["tool:edit_file", "contract:coding-agent", "tier:mvp"],
50
+ },
51
+ },
52
+ {
53
+ name: "run_shell",
54
+ title: "Run Shell",
55
+ kind: "shell",
56
+ tier: "mvp",
57
+ ...TOOL_POLICY_METADATA.run_shell,
58
+ runtimeDescriptor: "shell",
59
+ permissions: {
60
+ level: "elevated",
61
+ scopes: ["process:spawn", "filesystem:workspace"],
62
+ },
63
+ telemetry: {
64
+ category: "shell",
65
+ tags: ["tool:run_shell", "contract:coding-agent", "tier:mvp"],
66
+ },
67
+ },
68
+ {
69
+ name: "git",
70
+ title: "Git",
71
+ kind: "git",
72
+ tier: "mvp",
73
+ ...TOOL_POLICY_METADATA.git,
74
+ runtimeDescriptor: "git",
75
+ permissions: {
76
+ level: "elevated",
77
+ scopes: ["process:spawn", "filesystem:workspace", "vcs:git"],
78
+ },
79
+ telemetry: {
80
+ category: "git",
81
+ tags: ["tool:git", "contract:coding-agent", "tier:mvp"],
82
+ },
83
+ },
84
+ {
85
+ name: "search_files",
86
+ title: "Search Files",
87
+ kind: "filesystem",
88
+ tier: "mvp",
89
+ ...TOOL_POLICY_METADATA.search_files,
90
+ permissions: {
91
+ level: "readonly",
92
+ scopes: ["filesystem:read", "search:content"],
93
+ },
94
+ telemetry: {
95
+ category: "filesystem",
96
+ tags: ["tool:search_files", "contract:coding-agent", "tier:mvp"],
97
+ },
98
+ },
99
+ {
100
+ name: "list_dir",
101
+ title: "List Directory",
102
+ kind: "filesystem",
103
+ tier: "mvp",
104
+ ...TOOL_POLICY_METADATA.list_dir,
105
+ permissions: {
106
+ level: "readonly",
107
+ scopes: ["filesystem:read"],
108
+ },
109
+ telemetry: {
110
+ category: "filesystem",
111
+ tags: ["tool:list_dir", "contract:coding-agent", "tier:mvp"],
112
+ },
113
+ },
114
+ {
115
+ name: "run_skill",
116
+ title: "Run Skill",
117
+ kind: "skill",
118
+ tier: "extension",
119
+ ...TOOL_POLICY_METADATA.run_skill,
120
+ permissions: {
121
+ level: "standard",
122
+ scopes: ["skill:invoke"],
123
+ },
124
+ telemetry: {
125
+ category: "skill",
126
+ tags: ["tool:run_skill", "contract:coding-agent", "tier:extension"],
127
+ },
128
+ },
129
+ {
130
+ name: "list_skills",
131
+ title: "List Skills",
132
+ kind: "skill",
133
+ tier: "extension",
134
+ ...TOOL_POLICY_METADATA.list_skills,
135
+ permissions: {
136
+ level: "readonly",
137
+ scopes: ["skill:read"],
138
+ },
139
+ telemetry: {
140
+ category: "skill",
141
+ tags: ["tool:list_skills", "contract:coding-agent", "tier:extension"],
142
+ },
143
+ },
144
+ {
145
+ name: "run_code",
146
+ title: "Run Code",
147
+ kind: "code",
148
+ tier: "extension",
149
+ ...TOOL_POLICY_METADATA.run_code,
150
+ permissions: {
151
+ level: "elevated",
152
+ scopes: ["process:spawn", "filesystem:workspace", "runtime:script"],
153
+ },
154
+ telemetry: {
155
+ category: "code",
156
+ tags: ["tool:run_code", "contract:coding-agent", "tier:extension"],
157
+ },
158
+ },
159
+ {
160
+ name: "spawn_sub_agent",
161
+ title: "Spawn Sub Agent",
162
+ kind: "agent",
163
+ tier: "extension",
164
+ ...TOOL_POLICY_METADATA.spawn_sub_agent,
165
+ permissions: {
166
+ level: "elevated",
167
+ scopes: ["agent:spawn"],
168
+ },
169
+ telemetry: {
170
+ category: "agent",
171
+ tags: ["tool:spawn_sub_agent", "contract:coding-agent", "tier:extension"],
172
+ },
173
+ },
174
+ ]);
175
+
176
+ export const CODING_AGENT_MVP_TOOL_NAMES = Object.freeze(
177
+ CODING_AGENT_TOOL_CONTRACTS.filter((tool) => tool.tier === "mvp").map(
178
+ (tool) => tool.name,
179
+ ),
180
+ );
181
+
182
+ export const CODING_AGENT_EXTENSION_TOOL_NAMES = Object.freeze(
183
+ CODING_AGENT_TOOL_CONTRACTS.filter((tool) => tool.tier === "extension").map(
184
+ (tool) => tool.name,
185
+ ),
186
+ );
187
+
188
+ const TOOL_CONTRACT_MAP = new Map(
189
+ CODING_AGENT_TOOL_CONTRACTS.map((tool) => [tool.name, tool]),
190
+ );
191
+ const runtimeRegistry = createDefaultToolRegistry();
192
+
193
+ function cloneValue(value) {
194
+ return JSON.parse(JSON.stringify(value));
195
+ }
196
+
197
+ function getFallbackToolContract(name) {
198
+ return {
199
+ name,
200
+ title: name,
201
+ kind: "legacy",
202
+ tier: "legacy",
203
+ riskLevel: "medium",
204
+ availableInPlanMode: false,
205
+ planModeBehavior: "blocked",
206
+ requiresPlanApproval: false,
207
+ approvalFlow: "policy",
208
+ permissions: {
209
+ level: "standard",
210
+ scopes: [],
211
+ },
212
+ telemetry: {
213
+ category: "legacy",
214
+ tags: [`tool:${name}`, "contract:coding-agent", "tier:legacy"],
215
+ },
216
+ };
217
+ }
218
+
219
+ export function getCodingAgentToolContract(name) {
220
+ const tool = TOOL_CONTRACT_MAP.get(name);
221
+ return tool ? cloneValue(tool) : null;
222
+ }
223
+
224
+ export function getCodingAgentToolContracts({ tier = null } = {}) {
225
+ return CODING_AGENT_TOOL_CONTRACTS.filter((tool) => {
226
+ if (tier && tool.tier !== tier) {
227
+ return false;
228
+ }
229
+ return true;
230
+ }).map(cloneValue);
231
+ }
232
+
233
+ export function listCodingAgentToolNames({ tier = null } = {}) {
234
+ return getCodingAgentToolContracts({ tier }).map((tool) => tool.name);
235
+ }
236
+
237
+ export function isCodingAgentMvpTool(name) {
238
+ return CODING_AGENT_MVP_TOOL_NAMES.includes(name);
239
+ }
240
+
241
+ export function getCodingAgentToolPolicy(name) {
242
+ const tool = TOOL_CONTRACT_MAP.get(name);
243
+ if (!tool) {
244
+ return null;
245
+ }
246
+
247
+ return {
248
+ tier: tool.tier,
249
+ riskLevel: tool.riskLevel,
250
+ availableInPlanMode: tool.availableInPlanMode,
251
+ planModeBehavior: tool.planModeBehavior || "standard",
252
+ requiresPlanApproval: tool.requiresPlanApproval,
253
+ approvalFlow: tool.approvalFlow,
254
+ permissions: cloneValue(tool.permissions),
255
+ };
256
+ }
257
+
258
+ export function mapCodingAgentToolDefinition(definition = {}, options = {}) {
259
+ const fn = definition.function || {};
260
+ const name = fn.name;
261
+ if (!name) {
262
+ throw new Error("Coding agent tool definition requires function.name.");
263
+ }
264
+
265
+ const contract =
266
+ getCodingAgentToolContract(name) || getFallbackToolContract(name);
267
+
268
+ return {
269
+ name,
270
+ title: contract.title || name,
271
+ kind: contract.kind,
272
+ description: fn.description || "",
273
+ schema: fn.parameters || { type: "object", properties: {} },
274
+ permissions: cloneValue(contract.permissions),
275
+ telemetry: cloneValue(contract.telemetry),
276
+ source: options.source || "agent-core",
277
+ };
278
+ }
279
+
280
+ export function createCodingAgentToolRegistry(definitions = [], options = {}) {
281
+ const registry = new ToolRegistry();
282
+ definitions.forEach((definition) => {
283
+ registry.register(mapCodingAgentToolDefinition(definition, options));
284
+ });
285
+ return registry;
286
+ }
287
+
288
+ export function getCodingAgentRuntimeDescriptor(toolName) {
289
+ const descriptorName = TOOL_CONTRACT_MAP.get(toolName)?.runtimeDescriptor;
290
+ if (!descriptorName) {
291
+ return null;
292
+ }
293
+ return runtimeRegistry.get(descriptorName) || null;
294
+ }