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.
@@ -83,7 +83,14 @@ export class WSAgentHandler {
83
83
  contextEngine: session.contextEngine,
84
84
  hookDb: this.db,
85
85
  cwd: session.projectRoot,
86
+ sessionId: session.id,
87
+ planManager: session.planManager,
88
+ enabledToolNames: session.enabledToolNames || null,
86
89
  hostManagedToolPolicy: session.hostManagedToolPolicy || null,
90
+ extraToolDefinitions: session.externalToolDefinitions || [],
91
+ externalToolDescriptors: session.externalToolDescriptors || {},
92
+ externalToolExecutors: session.externalToolExecutors || {},
93
+ mcpClient: session.mcpClient || null,
87
94
  slotFiller,
88
95
  interaction: this.interaction,
89
96
  };
@@ -372,7 +379,7 @@ export class WSAgentHandler {
372
379
  planManager.approvePlan();
373
380
  this.session.messages.push({
374
381
  role: "system",
375
- content: `[PLAN APPROVED] The user has approved your plan with ${planManager.currentPlan.items.length} items. You can now use all tools including write_file, edit_file, run_shell, and run_skill. Execute the plan items in order.`,
382
+ content: `[PLAN APPROVED] The user has approved your plan with ${planManager.currentPlan.items.length} items. You can now use all tools including write_file, edit_file, run_shell, git, and run_skill. Execute the plan items in order.`,
376
383
  });
377
384
  this.interaction.emit("command-response", {
378
385
  requestId,
@@ -9,19 +9,31 @@
9
9
  import { createHash } from "crypto";
10
10
  import fs from "fs";
11
11
  import path from "path";
12
- import { PlanModeManager } from "./plan-mode.js";
12
+ import { ExecutionPlan, PlanModeManager, PlanState } from "./plan-mode.js";
13
13
  import { CLIContextEngineering } from "./cli-context-engineering.js";
14
14
  import { CLIPermanentMemory } from "./permanent-memory.js";
15
+ import {
16
+ createTrustedMcpServerMap,
17
+ resolveMcpServerPolicy,
18
+ normalizeRiskLevel,
19
+ normalizeBoolean,
20
+ selectHigherRiskLevel,
21
+ } from "../runtime/coding-agent-managed-tool-policy.cjs";
15
22
  import {
16
23
  createSession as dbCreateSession,
17
24
  saveMessages as dbSaveMessages,
18
25
  getSession as dbGetSession,
19
26
  listSessions as dbListSessions,
27
+ updateSession as dbUpdateSession,
20
28
  } from "./session-manager.js";
21
29
  import { buildSystemPrompt } from "./agent-core.js";
22
30
  import { SubAgentRegistry } from "./sub-agent-registry.js";
23
31
  import { createWorktree, removeWorktree } from "./worktree-isolator.js";
24
32
  import { isGitRepo } from "./git-integration.js";
33
+ import {
34
+ CODING_AGENT_MVP_TOOL_NAMES,
35
+ listCodingAgentToolNames,
36
+ } from "../runtime/coding-agent-contract.js";
25
37
 
26
38
  /**
27
39
  * @typedef {object} Session
@@ -36,7 +48,11 @@ import { isGitRepo } from "./git-integration.js";
36
48
  * @property {string} projectRoot
37
49
  * @property {string} baseProjectRoot
38
50
  * @property {string|null} rulesContent
51
+ * @property {string[]} enabledToolNames
39
52
  * @property {object|null} hostManagedToolPolicy
53
+ * @property {Array<object>} externalToolDefinitions
54
+ * @property {object} externalToolDescriptors
55
+ * @property {object} externalToolExecutors
40
56
  * @property {boolean} worktreeIsolation
41
57
  * @property {object|null} worktree
42
58
  * @property {PlanModeManager} planManager
@@ -58,11 +74,158 @@ export class WSSessionManager {
58
74
  this.db = options.db || null;
59
75
  this.config = options.config || {};
60
76
  this.defaultProjectRoot = options.defaultProjectRoot || process.cwd();
77
+ this.mcpClient = options.mcpClient || null;
78
+ this.allowedMcpServerNames = Array.isArray(options.allowedMcpServerNames)
79
+ ? options.allowedMcpServerNames
80
+ : null;
81
+ this.allowHighRiskMcpServers = options.allowHighRiskMcpServers === true;
82
+ this.trustedMcpServers = createTrustedMcpServerMap(
83
+ options.mcpServerRegistry || null,
84
+ );
61
85
 
62
86
  /** @type {Map<string, Session>} */
63
87
  this.sessions = new Map();
64
88
  }
65
89
 
90
+ _normalizeEnabledToolNames(enabledToolNames) {
91
+ const knownToolNames = new Set(listCodingAgentToolNames());
92
+ const requested = Array.isArray(enabledToolNames)
93
+ ? enabledToolNames
94
+ .map((name) => String(name || "").trim())
95
+ .filter(Boolean)
96
+ : [];
97
+
98
+ const filtered = requested.filter((name) => knownToolNames.has(name));
99
+ if (filtered.length > 0) {
100
+ return [...new Set(filtered)];
101
+ }
102
+
103
+ return [...CODING_AGENT_MVP_TOOL_NAMES];
104
+ }
105
+
106
+ _buildSessionExternalTools() {
107
+ if (
108
+ !this.mcpClient ||
109
+ !(this.mcpClient.servers instanceof Map) ||
110
+ typeof this.mcpClient.listTools !== "function"
111
+ ) {
112
+ return {
113
+ definitions: [],
114
+ descriptors: {},
115
+ executors: {},
116
+ };
117
+ }
118
+
119
+ const definitions = [];
120
+ const descriptors = {};
121
+ const executors = {};
122
+ const seenNames = new Set();
123
+
124
+ for (const [serverName, serverState] of this.mcpClient.servers.entries()) {
125
+ const serverPolicy = resolveMcpServerPolicy(serverName, serverState, {
126
+ allowedMcpServerNames: this.allowedMcpServerNames,
127
+ trustedMcpServers: this.trustedMcpServers,
128
+ allowHighRiskMcpServers: this.allowHighRiskMcpServers,
129
+ });
130
+
131
+ if (!serverPolicy.allowed) {
132
+ continue;
133
+ }
134
+
135
+ const serverTools = Array.isArray(serverState?.tools)
136
+ ? serverState.tools
137
+ : this.mcpClient.listTools(serverName);
138
+
139
+ for (const mcpTool of Array.isArray(serverTools) ? serverTools : []) {
140
+ const parsedSchema = this._parseToolSchema(mcpTool?.inputSchema) ||
141
+ this._parseToolSchema(mcpTool?.input_schema) ||
142
+ this._parseToolSchema(mcpTool?.parameters_schema) || {
143
+ type: "object",
144
+ properties: {},
145
+ };
146
+ const riskLevel = selectHigherRiskLevel(
147
+ serverPolicy.securityLevel,
148
+ normalizeRiskLevel(mcpTool?.risk_level, null),
149
+ );
150
+ const isReadOnly =
151
+ normalizeBoolean(mcpTool?.isReadOnly, false) ||
152
+ normalizeBoolean(mcpTool?.is_read_only, false) ||
153
+ riskLevel === "low";
154
+
155
+ let toolName = `mcp_${serverName}_${mcpTool?.name || "tool"}`;
156
+ if (seenNames.has(toolName)) {
157
+ let index = 2;
158
+ let candidate = `${toolName}_${index}`;
159
+ while (seenNames.has(candidate)) {
160
+ index += 1;
161
+ candidate = `${toolName}_${index}`;
162
+ }
163
+ toolName = candidate;
164
+ }
165
+ seenNames.add(toolName);
166
+
167
+ const descriptor = {
168
+ name: toolName,
169
+ description: mcpTool?.description || `MCP tool from ${serverName}.`,
170
+ inputSchema: parsedSchema,
171
+ isReadOnly,
172
+ riskLevel,
173
+ source: `mcp:${serverName}`,
174
+ mcpMetadata: {
175
+ serverName,
176
+ trusted: serverPolicy.trusted === true,
177
+ securityLevel: serverPolicy.securityLevel,
178
+ requiredPermissions: serverPolicy.requiredPermissions || [],
179
+ capabilities: serverPolicy.capabilities || [],
180
+ originalToolName: mcpTool?.name || null,
181
+ tool: mcpTool || null,
182
+ },
183
+ };
184
+
185
+ definitions.push({
186
+ type: "function",
187
+ function: {
188
+ name: descriptor.name,
189
+ description: descriptor.description,
190
+ parameters: JSON.parse(JSON.stringify(descriptor.inputSchema)),
191
+ },
192
+ });
193
+ descriptors[descriptor.name] = descriptor;
194
+ executors[descriptor.name] = {
195
+ kind: "mcp",
196
+ serverName,
197
+ toolName: mcpTool?.name || null,
198
+ };
199
+ }
200
+ }
201
+
202
+ return {
203
+ definitions,
204
+ descriptors,
205
+ executors,
206
+ };
207
+ }
208
+
209
+ _parseToolSchema(value) {
210
+ if (!value) {
211
+ return null;
212
+ }
213
+
214
+ if (typeof value === "object") {
215
+ return value;
216
+ }
217
+
218
+ if (typeof value !== "string") {
219
+ return null;
220
+ }
221
+
222
+ try {
223
+ return JSON.parse(value);
224
+ } catch (_err) {
225
+ return null;
226
+ }
227
+ }
228
+
66
229
  /**
67
230
  * Generate a unique session ID
68
231
  */
@@ -101,6 +264,10 @@ export class WSSessionManager {
101
264
  options.baseUrl || cfgLlm.baseUrl || "http://localhost:11434";
102
265
  const apiKey = options.apiKey || cfgLlm.apiKey || null;
103
266
  const worktreeIsolationRequested = options.worktreeIsolation === true;
267
+ const enabledToolNames = this._normalizeEnabledToolNames(
268
+ options.enabledToolNames,
269
+ );
270
+ const externalTools = this._buildSessionExternalTools();
104
271
  const isolatedWorkspace = this._prepareSessionWorkspace(
105
272
  baseProjectRoot,
106
273
  sessionId,
@@ -168,7 +335,12 @@ export class WSSessionManager {
168
335
  model,
169
336
  apiKey,
170
337
  baseUrl,
338
+ mcpClient: this.mcpClient,
339
+ enabledToolNames,
171
340
  hostManagedToolPolicy: options.hostManagedToolPolicy || null,
341
+ externalToolDefinitions: externalTools.definitions,
342
+ externalToolDescriptors: externalTools.descriptors,
343
+ externalToolExecutors: externalTools.executors,
172
344
  projectRoot,
173
345
  baseProjectRoot,
174
346
  rulesContent: null,
@@ -182,6 +354,17 @@ export class WSSessionManager {
182
354
  lastActivity: new Date().toISOString(),
183
355
  };
184
356
 
357
+ if (this.db) {
358
+ try {
359
+ dbUpdateSession(this.db, sessionId, {
360
+ metadata: this._serializeSessionMetadata(session),
361
+ });
362
+ } catch (_err) {
363
+ // Non-critical
364
+ }
365
+ }
366
+
367
+ this._bindPlanManagerPersistence(session);
185
368
  this.sessions.set(sessionId, session);
186
369
 
187
370
  return { sessionId };
@@ -213,13 +396,23 @@ export class WSSessionManager {
213
396
  typeof dbSession.messages === "string"
214
397
  ? JSON.parse(dbSession.messages)
215
398
  : dbSession.messages || [];
216
-
217
- const planManager = new PlanModeManager();
399
+ const metadata = this._normalizeSessionMetadata(dbSession.metadata);
400
+ const baseProjectRoot =
401
+ metadata.baseProjectRoot ||
402
+ metadata.projectRoot ||
403
+ this.defaultProjectRoot;
404
+ const workspace = this._restoreSessionWorkspace(
405
+ dbSession.id,
406
+ baseProjectRoot,
407
+ metadata,
408
+ );
409
+ const planManager = this._hydratePlanManager(metadata.planSnapshot);
410
+ const externalTools = this._buildSessionExternalTools();
218
411
  let contextEngine = null;
219
412
  let permanentMemory = null;
220
413
 
221
414
  try {
222
- const memoryDir = path.join(this.defaultProjectRoot, "memory");
415
+ const memoryDir = path.join(workspace.projectRoot, "memory");
223
416
  permanentMemory = new CLIPermanentMemory({
224
417
  db: this.db,
225
418
  memoryDir,
@@ -240,19 +433,26 @@ export class WSSessionManager {
240
433
 
241
434
  const session = {
242
435
  id: dbSession.id,
243
- type: "agent", // Default, since DB doesn't store type
436
+ type: metadata.sessionType || "agent",
244
437
  status: "active",
245
438
  messages,
246
439
  provider: dbSession.provider || "ollama",
247
440
  model: dbSession.model || null,
248
441
  apiKey: null,
249
- baseUrl: "http://localhost:11434",
250
- hostManagedToolPolicy: null,
251
- projectRoot: this.defaultProjectRoot,
252
- baseProjectRoot: this.defaultProjectRoot,
442
+ baseUrl: metadata.baseUrl || "http://localhost:11434",
443
+ mcpClient: this.mcpClient,
444
+ enabledToolNames: this._normalizeEnabledToolNames(
445
+ metadata.enabledToolNames,
446
+ ),
447
+ hostManagedToolPolicy: metadata.hostManagedToolPolicy || null,
448
+ externalToolDefinitions: externalTools.definitions,
449
+ externalToolDescriptors: externalTools.descriptors,
450
+ externalToolExecutors: externalTools.executors,
451
+ projectRoot: workspace.projectRoot,
452
+ baseProjectRoot,
253
453
  rulesContent: null,
254
- worktreeIsolation: false,
255
- worktree: null,
454
+ worktreeIsolation: metadata.worktreeIsolation === true,
455
+ worktree: workspace.worktree,
256
456
  planManager,
257
457
  contextEngine,
258
458
  permanentMemory,
@@ -261,6 +461,7 @@ export class WSSessionManager {
261
461
  lastActivity: new Date().toISOString(),
262
462
  };
263
463
 
464
+ this._bindPlanManagerPersistence(session);
264
465
  this.sessions.set(session.id, session);
265
466
  return session;
266
467
  } catch (_err) {
@@ -280,13 +481,7 @@ export class WSSessionManager {
280
481
  session.status = "closed";
281
482
 
282
483
  // Persist messages to DB
283
- if (this.db) {
284
- try {
285
- dbSaveMessages(this.db, sessionId, session.messages);
286
- } catch (_err) {
287
- // Non-critical
288
- }
289
- }
484
+ this._persistSessionState(sessionId);
290
485
 
291
486
  // Auto-summarize into permanent memory
292
487
  if (session.permanentMemory && session.messages.length > 4) {
@@ -306,6 +501,13 @@ export class WSSessionManager {
306
501
 
307
502
  // Clean up plan manager listeners
308
503
  if (session.planManager) {
504
+ if (typeof session._planPersistenceCleanup === "function") {
505
+ try {
506
+ session._planPersistenceCleanup();
507
+ } catch (_err) {
508
+ // Non-critical.
509
+ }
510
+ }
309
511
  session.planManager.removeAllListeners();
310
512
  }
311
513
 
@@ -339,6 +541,7 @@ export class WSSessionManager {
339
541
  provider: session.provider,
340
542
  model: session.model,
341
543
  messageCount: session.messages.length,
544
+ enabledToolNames: session.enabledToolNames || [],
342
545
  baseProjectRoot: session.baseProjectRoot,
343
546
  worktreeIsolation: session.worktreeIsolation === true,
344
547
  worktree: session.worktree || null,
@@ -353,14 +556,21 @@ export class WSSessionManager {
353
556
  const dbSessions = dbListSessions(this.db, { limit: 20 });
354
557
  const inMemoryIds = new Set(this.sessions.keys());
355
558
  for (const dbs of dbSessions) {
559
+ const metadata = this._normalizeSessionMetadata(dbs.metadata);
356
560
  if (!inMemoryIds.has(dbs.id)) {
357
561
  results.push({
358
562
  id: dbs.id,
359
- type: "unknown",
563
+ type: metadata.sessionType || "unknown",
360
564
  status: "persisted",
361
565
  provider: dbs.provider,
362
566
  model: dbs.model,
363
567
  messageCount: dbs.message_count,
568
+ enabledToolNames: Array.isArray(metadata.enabledToolNames)
569
+ ? metadata.enabledToolNames
570
+ : [],
571
+ baseProjectRoot: metadata.baseProjectRoot || null,
572
+ worktreeIsolation: metadata.worktreeIsolation === true,
573
+ worktree: metadata.worktree || null,
364
574
  createdAt: dbs.created_at,
365
575
  lastActivity: dbs.updated_at,
366
576
  });
@@ -397,6 +607,7 @@ export class WSSessionManager {
397
607
 
398
608
  session.hostManagedToolPolicy = hostManagedToolPolicy || null;
399
609
  session.lastActivity = new Date().toISOString();
610
+ this._persistSessionState(sessionId);
400
611
  return session;
401
612
  }
402
613
 
@@ -408,7 +619,12 @@ export class WSSessionManager {
408
619
  if (!session || !this.db) return;
409
620
 
410
621
  try {
411
- dbSaveMessages(this.db, sessionId, session.messages);
622
+ dbSaveMessages(
623
+ this.db,
624
+ sessionId,
625
+ session.messages,
626
+ this._serializeSessionMetadata(session),
627
+ );
412
628
  } catch (_err) {
413
629
  // Non-critical
414
630
  }
@@ -442,4 +658,156 @@ export class WSSessionManager {
442
658
  },
443
659
  };
444
660
  }
661
+
662
+ _persistSessionState(sessionId) {
663
+ const session = this.sessions.get(sessionId);
664
+ if (!session || !this.db) return;
665
+
666
+ try {
667
+ dbSaveMessages(
668
+ this.db,
669
+ sessionId,
670
+ session.messages,
671
+ this._serializeSessionMetadata(session),
672
+ );
673
+ } catch (_err) {
674
+ // Non-critical
675
+ }
676
+
677
+ session.lastActivity = new Date().toISOString();
678
+ }
679
+
680
+ _serializeSessionMetadata(session) {
681
+ return {
682
+ version: 1,
683
+ sessionType: session.type || "agent",
684
+ projectRoot: session.projectRoot || null,
685
+ baseProjectRoot: session.baseProjectRoot || session.projectRoot || null,
686
+ baseUrl: session.baseUrl || null,
687
+ hostManagedToolPolicy: session.hostManagedToolPolicy || null,
688
+ enabledToolNames: session.enabledToolNames || [],
689
+ worktreeIsolation: session.worktreeIsolation === true,
690
+ worktree: session.worktree || null,
691
+ planSnapshot: this._serializePlanManager(session.planManager),
692
+ };
693
+ }
694
+
695
+ _serializePlanManager(planManager) {
696
+ if (!planManager) {
697
+ return null;
698
+ }
699
+
700
+ return {
701
+ state: planManager.state || PlanState.INACTIVE,
702
+ currentPlan: planManager.currentPlan || null,
703
+ history: Array.isArray(planManager.history) ? planManager.history : [],
704
+ blockedToolLog: Array.isArray(planManager.blockedToolLog)
705
+ ? planManager.blockedToolLog
706
+ : [],
707
+ };
708
+ }
709
+
710
+ _normalizeSessionMetadata(metadata) {
711
+ if (!metadata) {
712
+ return {};
713
+ }
714
+
715
+ if (typeof metadata === "string") {
716
+ try {
717
+ return JSON.parse(metadata);
718
+ } catch (_err) {
719
+ return {};
720
+ }
721
+ }
722
+
723
+ return typeof metadata === "object" ? metadata : {};
724
+ }
725
+
726
+ _hydratePlanManager(snapshot) {
727
+ const planManager = new PlanModeManager();
728
+ if (!snapshot || typeof snapshot !== "object") {
729
+ return planManager;
730
+ }
731
+
732
+ planManager.state = snapshot.state || PlanState.INACTIVE;
733
+ planManager.currentPlan = snapshot.currentPlan
734
+ ? new ExecutionPlan(snapshot.currentPlan)
735
+ : null;
736
+ planManager.history = Array.isArray(snapshot.history)
737
+ ? snapshot.history.map((plan) => new ExecutionPlan(plan))
738
+ : [];
739
+ planManager.blockedToolLog = Array.isArray(snapshot.blockedToolLog)
740
+ ? [...snapshot.blockedToolLog]
741
+ : [];
742
+ return planManager;
743
+ }
744
+
745
+ _restoreSessionWorkspace(sessionId, baseProjectRoot, metadata = {}) {
746
+ const requestedWorktreeIsolation = metadata.worktreeIsolation === true;
747
+ const persistedWorktreePath = metadata.worktree?.path || null;
748
+
749
+ if (!requestedWorktreeIsolation) {
750
+ return {
751
+ projectRoot: metadata.projectRoot || baseProjectRoot,
752
+ worktree: null,
753
+ };
754
+ }
755
+
756
+ if (persistedWorktreePath && fs.existsSync(persistedWorktreePath)) {
757
+ return {
758
+ projectRoot: persistedWorktreePath,
759
+ worktree: {
760
+ ...(metadata.worktree || {}),
761
+ baseProjectRoot,
762
+ },
763
+ };
764
+ }
765
+
766
+ try {
767
+ return this._prepareSessionWorkspace(baseProjectRoot, sessionId, {
768
+ worktreeIsolation: true,
769
+ });
770
+ } catch (_err) {
771
+ return {
772
+ projectRoot: baseProjectRoot,
773
+ worktree: null,
774
+ };
775
+ }
776
+ }
777
+
778
+ _bindPlanManagerPersistence(session) {
779
+ if (
780
+ !session?.id ||
781
+ !session.planManager ||
782
+ typeof session.planManager.on !== "function"
783
+ ) {
784
+ return;
785
+ }
786
+
787
+ if (typeof session._planPersistenceCleanup === "function") {
788
+ session._planPersistenceCleanup();
789
+ }
790
+
791
+ const persist = () => this._persistSessionState(session.id);
792
+ const events = [
793
+ "enter",
794
+ "exit",
795
+ "item-added",
796
+ "plan-ready",
797
+ "plan-approved",
798
+ "tool-blocked",
799
+ ];
800
+
801
+ for (const eventName of events) {
802
+ session.planManager.on(eventName, persist);
803
+ }
804
+
805
+ session._planPersistenceCleanup = () => {
806
+ if (typeof session.planManager.off === "function") {
807
+ for (const eventName of events) {
808
+ session.planManager.off(eventName, persist);
809
+ }
810
+ }
811
+ };
812
+ }
445
813
  }