@dexto/core 1.6.24 → 1.6.26

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 (80) hide show
  1. package/dist/agent/DextoAgent.cjs +52 -17
  2. package/dist/agent/DextoAgent.d.ts +11 -6
  3. package/dist/agent/DextoAgent.d.ts.map +1 -1
  4. package/dist/agent/DextoAgent.js +52 -17
  5. package/dist/agent/state-manager.cjs +6 -0
  6. package/dist/agent/state-manager.d.ts +4 -0
  7. package/dist/agent/state-manager.d.ts.map +1 -1
  8. package/dist/agent/state-manager.js +6 -0
  9. package/dist/approval/manager.cjs +328 -178
  10. package/dist/approval/manager.d.ts +39 -31
  11. package/dist/approval/manager.d.ts.map +1 -1
  12. package/dist/approval/manager.js +328 -178
  13. package/dist/approval/session-approval-store.cjs +91 -0
  14. package/dist/approval/session-approval-store.d.ts +55 -0
  15. package/dist/approval/session-approval-store.d.ts.map +1 -0
  16. package/dist/approval/session-approval-store.js +68 -0
  17. package/dist/llm/executor/stream-processor.cjs +24 -15
  18. package/dist/llm/executor/stream-processor.d.ts +5 -0
  19. package/dist/llm/executor/stream-processor.d.ts.map +1 -1
  20. package/dist/llm/executor/stream-processor.js +24 -15
  21. package/dist/llm/executor/turn-executor.cjs +7 -3
  22. package/dist/llm/executor/turn-executor.d.ts.map +1 -1
  23. package/dist/llm/executor/turn-executor.js +7 -3
  24. package/dist/llm/services/factory.cjs +10 -4
  25. package/dist/llm/services/factory.d.ts +2 -21
  26. package/dist/llm/services/factory.d.ts.map +1 -1
  27. package/dist/llm/services/factory.js +11 -7
  28. package/dist/llm/services/types.d.ts +33 -2
  29. package/dist/llm/services/types.d.ts.map +1 -1
  30. package/dist/llm/services/vercel.cjs +4 -5
  31. package/dist/llm/services/vercel.d.ts +3 -3
  32. package/dist/llm/services/vercel.d.ts.map +1 -1
  33. package/dist/llm/services/vercel.js +2 -3
  34. package/dist/logger/default-logger-factory.d.ts +12 -12
  35. package/dist/logger/v2/schemas.d.ts +6 -6
  36. package/dist/mcp/schemas.d.ts +10 -10
  37. package/dist/session/chat-session.cjs +39 -41
  38. package/dist/session/chat-session.d.ts +22 -12
  39. package/dist/session/chat-session.d.ts.map +1 -1
  40. package/dist/session/chat-session.js +39 -41
  41. package/dist/session/message-queue-store.cjs +75 -0
  42. package/dist/session/message-queue-store.d.ts +16 -0
  43. package/dist/session/message-queue-store.d.ts.map +1 -0
  44. package/dist/session/message-queue-store.js +52 -0
  45. package/dist/session/message-queue.cjs +140 -46
  46. package/dist/session/message-queue.d.ts +18 -6
  47. package/dist/session/message-queue.d.ts.map +1 -1
  48. package/dist/session/message-queue.js +140 -46
  49. package/dist/session/session-manager.cjs +130 -25
  50. package/dist/session/session-manager.d.ts +18 -1
  51. package/dist/session/session-manager.d.ts.map +1 -1
  52. package/dist/session/session-manager.js +130 -25
  53. package/dist/session/title-generator.cjs +9 -2
  54. package/dist/session/title-generator.d.ts +2 -0
  55. package/dist/session/title-generator.d.ts.map +1 -1
  56. package/dist/session/title-generator.js +9 -2
  57. package/dist/telemetry/errors.cjs +2 -2
  58. package/dist/telemetry/errors.js +2 -2
  59. package/dist/telemetry/index.d.ts +1 -1
  60. package/dist/telemetry/index.d.ts.map +1 -1
  61. package/dist/telemetry/index.js +3 -1
  62. package/dist/telemetry/telemetry.cjs +62 -21
  63. package/dist/telemetry/telemetry.d.ts +14 -0
  64. package/dist/telemetry/telemetry.d.ts.map +1 -1
  65. package/dist/telemetry/telemetry.js +62 -21
  66. package/dist/test-utils/session-state-stores.cjs +68 -0
  67. package/dist/test-utils/session-state-stores.js +42 -0
  68. package/dist/tools/session-tool-preferences-store.cjs +86 -0
  69. package/dist/tools/session-tool-preferences-store.d.ts +29 -0
  70. package/dist/tools/session-tool-preferences-store.d.ts.map +1 -0
  71. package/dist/tools/session-tool-preferences-store.js +63 -0
  72. package/dist/tools/tool-manager.cjs +131 -32
  73. package/dist/tools/tool-manager.d.ts +17 -6
  74. package/dist/tools/tool-manager.d.ts.map +1 -1
  75. package/dist/tools/tool-manager.js +131 -32
  76. package/dist/utils/service-initializer.cjs +38 -5
  77. package/dist/utils/service-initializer.d.ts +11 -1
  78. package/dist/utils/service-initializer.d.ts.map +1 -1
  79. package/dist/utils/service-initializer.js +36 -4
  80. package/package.json +1 -1
@@ -6,6 +6,7 @@ import { createApprovalRequest } from "./factory.js";
6
6
  import { DextoLogComponent } from "../logger/v2/types.js";
7
7
  import { ApprovalError } from "./errors.js";
8
8
  import { patternCovers } from "../tools/pattern-utils.js";
9
+ const GLOBAL_APPROVAL_SCOPE = "__global__";
9
10
  function tryRealpathSync(targetPath) {
10
11
  try {
11
12
  return realpathSync(targetPath);
@@ -31,45 +32,156 @@ function tryRealpathSyncWithExistingParent(resolvedPath) {
31
32
  }
32
33
  }
33
34
  class ApprovalManager {
34
- handler;
35
- config;
36
- logger;
37
- /**
38
- * Tool approval patterns, keyed by tool id.
39
- *
40
- * Patterns use simple glob syntax (e.g. "git *", "npm install *") and are matched
41
- * using pattern-to-pattern covering (see {@link patternCovers}).
42
- */
43
- toolPatterns = /* @__PURE__ */ new Map();
44
- /**
45
- * Directories approved for file access for the current session.
46
- * Stores normalized absolute paths mapped to their approval type:
47
- * - 'session': No directory prompt, follows tool config (working dir + user session-approved)
48
- * - 'once': Prompts each time, but tool can execute
49
- * Cleared when session ends.
50
- */
51
- approvedDirectories = /* @__PURE__ */ new Map();
52
- constructor(config, logger) {
35
+ constructor(config, logger, sessionApprovalStore) {
36
+ this.sessionApprovalStore = sessionApprovalStore;
53
37
  this.config = config;
54
38
  this.logger = logger.createChild(DextoLogComponent.APPROVAL);
55
39
  this.logger.debug(
56
40
  `ApprovalManager initialized with permissions.mode: ${config.permissions.mode}, elicitation.enabled: ${config.elicitation.enabled}`
57
41
  );
58
42
  }
43
+ handler;
44
+ config;
45
+ logger;
46
+ loadedScopes = /* @__PURE__ */ new Set();
47
+ scopeLocks = /* @__PURE__ */ new Map();
48
+ scopes = /* @__PURE__ */ new Map();
49
+ getScopeKey(sessionId) {
50
+ return sessionId ?? GLOBAL_APPROVAL_SCOPE;
51
+ }
52
+ getScopeLabel(sessionId) {
53
+ return sessionId ?? "global";
54
+ }
55
+ getApprovalTimeout(type, timeout) {
56
+ return timeout ?? this.getDefaultTimeout(type);
57
+ }
58
+ getDefaultTimeout(type) {
59
+ return type === ApprovalType.ELICITATION ? this.config.elicitation.timeout : this.config.permissions.timeout;
60
+ }
61
+ createEmptyScopeState() {
62
+ return {
63
+ toolPatterns: /* @__PURE__ */ new Map(),
64
+ approvedDirectories: /* @__PURE__ */ new Map()
65
+ };
66
+ }
67
+ getOrCreateScope(scopeKey) {
68
+ const existing = this.scopes.get(scopeKey);
69
+ if (existing) return existing;
70
+ const created = this.createEmptyScopeState();
71
+ this.scopes.set(scopeKey, created);
72
+ return created;
73
+ }
74
+ getScope(scopeKey) {
75
+ return this.scopes.get(scopeKey) ?? this.createEmptyScopeState();
76
+ }
77
+ async runWithScopeLock(scopeKey, fn) {
78
+ const previousLock = this.scopeLocks.get(scopeKey) ?? Promise.resolve();
79
+ const currentResult = previousLock.catch(() => {
80
+ }).then(() => fn());
81
+ const currentLock = currentResult.then(
82
+ () => void 0,
83
+ () => void 0
84
+ );
85
+ this.scopeLocks.set(scopeKey, currentLock);
86
+ try {
87
+ return await currentResult;
88
+ } finally {
89
+ if (this.scopeLocks.get(scopeKey) === currentLock) {
90
+ this.scopeLocks.delete(scopeKey);
91
+ }
92
+ }
93
+ }
94
+ snapshotToolPatterns(scopeKey) {
95
+ const snapshot = {};
96
+ for (const [toolName, patterns] of this.getScope(scopeKey).toolPatterns) {
97
+ snapshot[toolName] = Array.from(patterns);
98
+ }
99
+ return snapshot;
100
+ }
101
+ snapshotApprovedDirectories(scopeKey) {
102
+ return Array.from(this.getScope(scopeKey).approvedDirectories.entries()).map(
103
+ ([path2, type]) => ({
104
+ path: path2,
105
+ type
106
+ })
107
+ );
108
+ }
109
+ async persistScope(sessionId) {
110
+ const scopeKey = this.getScopeKey(sessionId);
111
+ const state = {
112
+ toolPatterns: this.snapshotToolPatterns(scopeKey),
113
+ approvedDirectories: this.snapshotApprovedDirectories(scopeKey)
114
+ };
115
+ await this.sessionApprovalStore.save(sessionId, state);
116
+ }
117
+ hydrateScope(sessionId, state) {
118
+ const scopeKey = this.getScopeKey(sessionId);
119
+ const toolPatterns = /* @__PURE__ */ new Map();
120
+ for (const [toolName, patterns] of Object.entries(state.toolPatterns)) {
121
+ toolPatterns.set(toolName, new Set(patterns));
122
+ }
123
+ const approvedDirectories = /* @__PURE__ */ new Map();
124
+ for (const entry of state.approvedDirectories) {
125
+ approvedDirectories.set(entry.path, entry.type);
126
+ }
127
+ this.scopes.set(scopeKey, {
128
+ toolPatterns,
129
+ approvedDirectories
130
+ });
131
+ }
132
+ async restoreSessionState(sessionId) {
133
+ const scopeKey = this.getScopeKey(sessionId);
134
+ if (this.loadedScopes.has(scopeKey)) {
135
+ return;
136
+ }
137
+ await this.runWithScopeLock(scopeKey, async () => {
138
+ if (this.loadedScopes.has(scopeKey)) {
139
+ return;
140
+ }
141
+ const state = await this.sessionApprovalStore.load(sessionId);
142
+ this.hydrateScope(sessionId, state);
143
+ this.loadedScopes.add(scopeKey);
144
+ this.logger.debug("Restored persisted approval state", {
145
+ sessionId: this.getScopeLabel(sessionId),
146
+ toolCount: Object.keys(state.toolPatterns).length,
147
+ directoryCount: state.approvedDirectories.length
148
+ });
149
+ });
150
+ }
151
+ evictSessionState(sessionId) {
152
+ const scopeKey = this.getScopeKey(sessionId);
153
+ this.scopes.delete(scopeKey);
154
+ this.loadedScopes.delete(scopeKey);
155
+ }
156
+ async deleteSessionState(sessionId) {
157
+ const scopeKey = this.getScopeKey(sessionId);
158
+ await this.runWithScopeLock(scopeKey, async () => {
159
+ this.evictSessionState(sessionId);
160
+ await this.sessionApprovalStore.delete(sessionId);
161
+ });
162
+ }
59
163
  // ==================== Pattern Methods ====================
60
- getOrCreateToolPatternSet(toolName) {
61
- const existing = this.toolPatterns.get(toolName);
164
+ getOrCreateToolPatternSet(toolName, scopeKey) {
165
+ const scope = this.getOrCreateScope(scopeKey).toolPatterns;
166
+ const existing = scope.get(toolName);
62
167
  if (existing) return existing;
63
168
  const created = /* @__PURE__ */ new Set();
64
- this.toolPatterns.set(toolName, created);
169
+ scope.set(toolName, created);
65
170
  return created;
66
171
  }
67
172
  /**
68
173
  * Add an approval pattern for a tool.
69
174
  */
70
- addPattern(toolName, pattern) {
71
- this.getOrCreateToolPatternSet(toolName).add(pattern);
72
- this.logger.debug(`Added pattern for '${toolName}': "${pattern}"`);
175
+ async addPattern(toolName, pattern, sessionId) {
176
+ await this.restoreSessionState(sessionId);
177
+ const scopeKey = this.getScopeKey(sessionId);
178
+ await this.runWithScopeLock(scopeKey, async () => {
179
+ this.getOrCreateToolPatternSet(toolName, scopeKey).add(pattern);
180
+ await this.persistScope(sessionId);
181
+ });
182
+ this.logger.debug(
183
+ `Added pattern for '${toolName}' in '${this.getScopeLabel(sessionId)}': "${pattern}"`
184
+ );
73
185
  }
74
186
  /**
75
187
  * Check if a pattern key is covered by any approved pattern for a tool.
@@ -77,8 +189,9 @@ class ApprovalManager {
77
189
  * Note: This expects a pattern key (e.g. "git push *"), not raw arguments.
78
190
  * Tools are responsible for generating the key via `tool.approval.patternKey()`.
79
191
  */
80
- matchesPattern(toolName, patternKey) {
81
- const patterns = this.toolPatterns.get(toolName);
192
+ matchesPattern(toolName, patternKey, sessionId) {
193
+ const scopeKey = this.getScopeKey(sessionId);
194
+ const patterns = this.getScope(scopeKey).toolPatterns.get(toolName);
82
195
  if (!patterns || patterns.size === 0) return false;
83
196
  for (const storedPattern of patterns) {
84
197
  if (patternCovers(storedPattern, patternKey)) {
@@ -93,37 +206,47 @@ class ApprovalManager {
93
206
  /**
94
207
  * Clear all patterns for a tool (or all tools when omitted).
95
208
  */
96
- clearPatterns(toolName) {
97
- if (toolName) {
98
- const patterns = this.toolPatterns.get(toolName);
99
- if (!patterns) return;
100
- const count2 = patterns.size;
101
- patterns.clear();
102
- if (count2 > 0) {
103
- this.logger.debug(`Cleared ${count2} pattern(s) for '${toolName}'`);
209
+ async clearPatterns(toolName, sessionId) {
210
+ await this.restoreSessionState(sessionId);
211
+ const scopeKey = this.getScopeKey(sessionId);
212
+ await this.runWithScopeLock(scopeKey, async () => {
213
+ const scope = this.getOrCreateScope(scopeKey).toolPatterns;
214
+ if (toolName) {
215
+ const patterns = scope.get(toolName);
216
+ if (!patterns) return;
217
+ const count2 = patterns.size;
218
+ scope.delete(toolName);
219
+ await this.persistScope(sessionId);
220
+ if (count2 > 0) {
221
+ this.logger.debug(
222
+ `Cleared ${count2} pattern(s) for '${toolName}' in '${this.getScopeLabel(sessionId)}'`
223
+ );
224
+ }
225
+ return;
104
226
  }
105
- return;
106
- }
107
- const count = Array.from(this.toolPatterns.values()).reduce(
108
- (sum, set) => sum + set.size,
109
- 0
110
- );
111
- this.toolPatterns.clear();
112
- if (count > 0) {
113
- this.logger.debug(`Cleared ${count} total tool pattern(s)`);
114
- }
227
+ const count = Array.from(scope.values()).reduce((sum, set) => sum + set.size, 0);
228
+ scope.clear();
229
+ await this.persistScope(sessionId);
230
+ if (count > 0) {
231
+ this.logger.debug(
232
+ `Cleared ${count} total tool pattern(s) in '${this.getScopeLabel(sessionId)}'`
233
+ );
234
+ }
235
+ });
115
236
  }
116
237
  /**
117
238
  * Get patterns for a tool (for debugging/display).
118
239
  */
119
- getToolPatterns(toolName) {
120
- return this.toolPatterns.get(toolName) ?? /* @__PURE__ */ new Set();
240
+ getToolPatterns(toolName, sessionId) {
241
+ const scopeKey = this.getScopeKey(sessionId);
242
+ return this.getScope(scopeKey).toolPatterns.get(toolName) ?? /* @__PURE__ */ new Set();
121
243
  }
122
244
  /**
123
245
  * Get all tool patterns (for debugging/display).
124
246
  */
125
- getAllToolPatterns() {
126
- return this.toolPatterns;
247
+ getAllToolPatterns(sessionId) {
248
+ const scopeKey = this.getScopeKey(sessionId);
249
+ return this.getScope(scopeKey).toolPatterns;
127
250
  }
128
251
  // ==================== Directory Access Methods ====================
129
252
  /**
@@ -133,21 +256,32 @@ class ApprovalManager {
133
256
  * continue to work even when other subsystems canonicalize paths via realpath
134
257
  * (e.g. macOS /tmp -> /private/tmp or custom symlinked directories).
135
258
  */
136
- getDirectoryApprovalKeys(directory) {
137
- const resolved = path.resolve(directory);
259
+ getPathApprovalKeys(targetPath) {
260
+ const resolved = path.resolve(targetPath);
138
261
  const real = tryRealpathSyncWithExistingParent(resolved);
139
262
  if (real && real !== resolved) {
140
263
  return [resolved, real];
141
264
  }
142
265
  return [resolved];
143
266
  }
144
- getFileApprovalKeys(filePath) {
145
- const resolved = path.resolve(filePath);
146
- const real = tryRealpathSyncWithExistingParent(resolved);
147
- if (real && real !== resolved) {
148
- return [resolved, real];
267
+ isPathWithinApprovedDirectory(targetPath, sessionId, approvedTypes) {
268
+ const scopeKey = this.getScopeKey(sessionId);
269
+ const directoryScope = this.getScope(scopeKey).approvedDirectories;
270
+ for (const normalized of this.getPathApprovalKeys(targetPath)) {
271
+ for (const [approvedDir, type] of directoryScope) {
272
+ if (!approvedTypes.has(type)) {
273
+ continue;
274
+ }
275
+ const relative = path.relative(approvedDir, normalized);
276
+ if (!relative.startsWith("..") && !path.isAbsolute(relative)) {
277
+ this.logger.debug(
278
+ `Path "${normalized}" is within approved directory "${approvedDir}" (type: ${type})`
279
+ );
280
+ return true;
281
+ }
282
+ }
149
283
  }
150
- return [resolved];
284
+ return false;
151
285
  }
152
286
  /**
153
287
  * Initialize the working directory as a session-approved directory.
@@ -156,8 +290,8 @@ class ApprovalManager {
156
290
  *
157
291
  * @param workingDir The working directory path
158
292
  */
159
- initializeWorkingDirectory(workingDir) {
160
- this.addApprovedDirectory(workingDir, "session");
293
+ async initializeWorkingDirectory(workingDir, sessionId) {
294
+ await this.addApprovedDirectory(workingDir, "session", sessionId);
161
295
  }
162
296
  /**
163
297
  * Add a directory to the approved list for this session.
@@ -176,29 +310,35 @@ class ApprovalManager {
176
310
  * // Tool can access, but will prompt again next time
177
311
  * ```
178
312
  */
179
- addApprovedDirectory(directory, type = "session") {
180
- const keys = this.getDirectoryApprovalKeys(directory);
181
- const existingTypes = keys.map((key) => this.approvedDirectories.get(key)).filter((value) => value !== void 0);
182
- const hasSessionApproval = existingTypes.includes("session");
183
- const effectiveType = type === "session" || hasSessionApproval ? "session" : "once";
184
- for (const key of keys) {
185
- const existing = this.approvedDirectories.get(key);
186
- if (existing === "session") {
187
- continue;
313
+ async addApprovedDirectory(directory, type = "session", sessionId) {
314
+ await this.restoreSessionState(sessionId);
315
+ const scopeKey = this.getScopeKey(sessionId);
316
+ await this.runWithScopeLock(scopeKey, async () => {
317
+ const keys = this.getPathApprovalKeys(directory);
318
+ const directoryScope = this.getOrCreateScope(scopeKey).approvedDirectories;
319
+ const existingTypes = keys.map((key) => directoryScope.get(key)).filter((value) => value !== void 0);
320
+ const hasSessionApproval = existingTypes.includes("session");
321
+ const effectiveType = type === "session" || hasSessionApproval ? "session" : "once";
322
+ for (const key of keys) {
323
+ const existing = directoryScope.get(key);
324
+ if (existing === "session") {
325
+ continue;
326
+ }
327
+ directoryScope.set(key, effectiveType);
188
328
  }
189
- this.approvedDirectories.set(key, effectiveType);
190
- }
191
- const resolvedKey = keys[0];
192
- if (effectiveType === "session" && type === "once" && hasSessionApproval) {
329
+ await this.persistScope(sessionId);
330
+ const resolvedKey = keys[0];
331
+ if (effectiveType === "session" && type === "once" && hasSessionApproval) {
332
+ this.logger.debug(
333
+ `Directory "${resolvedKey}" already approved as 'session', not downgrading to 'once'`
334
+ );
335
+ return;
336
+ }
337
+ const realKey = keys.length > 1 ? keys[1] : null;
193
338
  this.logger.debug(
194
- `Directory "${resolvedKey}" already approved as 'session', not downgrading to 'once'`
339
+ `Added approved directory in '${this.getScopeLabel(sessionId)}': "${resolvedKey}" (type: ${effectiveType})${realKey ? `, realpath: "${realKey}"` : ""}`
195
340
  );
196
- return;
197
- }
198
- const realKey = keys.length > 1 ? keys[1] : null;
199
- this.logger.debug(
200
- `Added approved directory: "${resolvedKey}" (type: ${effectiveType})${realKey ? `, realpath: "${realKey}"` : ""}`
201
- );
341
+ });
202
342
  }
203
343
  /**
204
344
  * Check if a file path is within any session-approved directory.
@@ -208,20 +348,8 @@ class ApprovalManager {
208
348
  * @param filePath The file path to check (can be relative or absolute)
209
349
  * @returns true if the path is within a session-approved directory
210
350
  */
211
- isDirectorySessionApproved(filePath) {
212
- for (const normalized of this.getFileApprovalKeys(filePath)) {
213
- for (const [approvedDir, type] of this.approvedDirectories) {
214
- if (type !== "session") continue;
215
- const relative = path.relative(approvedDir, normalized);
216
- if (!relative.startsWith("..") && !path.isAbsolute(relative)) {
217
- this.logger.debug(
218
- `Path "${normalized}" is within session-approved directory "${approvedDir}"`
219
- );
220
- return true;
221
- }
222
- }
223
- }
224
- return false;
351
+ isDirectorySessionApproved(filePath, sessionId) {
352
+ return this.isPathWithinApprovedDirectory(filePath, sessionId, /* @__PURE__ */ new Set(["session"]));
225
353
  }
226
354
  /**
227
355
  * Check if a file path is within any approved directory (session OR once).
@@ -231,51 +359,99 @@ class ApprovalManager {
231
359
  * @param filePath The file path to check (can be relative or absolute)
232
360
  * @returns true if the path is within any approved directory
233
361
  */
234
- isDirectoryApproved(filePath) {
235
- for (const normalized of this.getFileApprovalKeys(filePath)) {
236
- for (const [approvedDir] of this.approvedDirectories) {
237
- const relative = path.relative(approvedDir, normalized);
238
- if (!relative.startsWith("..") && !path.isAbsolute(relative)) {
239
- this.logger.debug(
240
- `Path "${normalized}" is within approved directory "${approvedDir}"`
241
- );
242
- return true;
243
- }
244
- }
245
- }
246
- return false;
362
+ isDirectoryApproved(filePath, sessionId) {
363
+ return this.isPathWithinApprovedDirectory(
364
+ filePath,
365
+ sessionId,
366
+ /* @__PURE__ */ new Set(["session", "once"])
367
+ );
247
368
  }
248
369
  /**
249
370
  * Clear all approved directories.
250
371
  * Should be called when session ends.
251
372
  */
252
- clearApprovedDirectories() {
253
- const count = this.approvedDirectories.size;
254
- this.approvedDirectories.clear();
255
- if (count > 0) {
256
- this.logger.debug(`Cleared ${count} approved directories`);
257
- }
373
+ async clearApprovedDirectories(sessionId) {
374
+ await this.restoreSessionState(sessionId);
375
+ const scopeKey = this.getScopeKey(sessionId);
376
+ await this.runWithScopeLock(scopeKey, async () => {
377
+ const scope = this.getOrCreateScope(scopeKey).approvedDirectories;
378
+ const count = scope.size;
379
+ scope.clear();
380
+ await this.persistScope(sessionId);
381
+ if (count > 0) {
382
+ this.logger.debug(
383
+ `Cleared ${count} approved directories in '${this.getScopeLabel(sessionId)}'`
384
+ );
385
+ }
386
+ });
258
387
  }
259
388
  /**
260
389
  * Get the current map of approved directories with their types (for debugging/display).
261
390
  */
262
- getApprovedDirectories() {
263
- return this.approvedDirectories;
391
+ getApprovedDirectories(sessionId) {
392
+ const scopeKey = this.getScopeKey(sessionId);
393
+ return this.getScope(scopeKey).approvedDirectories;
264
394
  }
265
395
  /**
266
396
  * Get just the directory paths that are approved (for debugging/display).
267
397
  */
268
- getApprovedDirectoryPaths() {
269
- return Array.from(this.approvedDirectories.keys());
398
+ getApprovedDirectoryPaths(sessionId) {
399
+ return Array.from(this.getApprovedDirectories(sessionId).keys());
270
400
  }
271
401
  /**
272
402
  * Clear all session-scoped approvals (tool patterns and directories).
273
403
  * Convenience method for clearing all session state at once.
274
404
  */
275
- clearSessionApprovals() {
276
- this.clearPatterns();
277
- this.clearApprovedDirectories();
278
- this.logger.debug("Cleared all session approvals");
405
+ async clearSessionApprovals(sessionId) {
406
+ await this.restoreSessionState(sessionId);
407
+ const scopeKey = this.getScopeKey(sessionId);
408
+ await this.runWithScopeLock(scopeKey, async () => {
409
+ const scope = this.getOrCreateScope(scopeKey);
410
+ const patternCount = Array.from(scope.toolPatterns.values()).reduce(
411
+ (sum, set) => sum + set.size,
412
+ 0
413
+ );
414
+ const directoryCount = scope.approvedDirectories.size;
415
+ scope.toolPatterns.clear();
416
+ scope.approvedDirectories.clear();
417
+ await this.persistScope(sessionId);
418
+ if (patternCount > 0 || directoryCount > 0) {
419
+ this.logger.debug(
420
+ `Cleared ${patternCount} tool pattern(s) and ${directoryCount} approved director${directoryCount === 1 ? "y" : "ies"} in '${this.getScopeLabel(sessionId)}'`
421
+ );
422
+ }
423
+ });
424
+ }
425
+ createApprovalDetails(type, metadata, sessionId, timeout) {
426
+ const details = {
427
+ type,
428
+ timeout: this.getApprovalTimeout(type, timeout),
429
+ metadata
430
+ };
431
+ if (sessionId !== void 0) {
432
+ details.sessionId = sessionId;
433
+ }
434
+ return details;
435
+ }
436
+ createResponse(request, response) {
437
+ return {
438
+ approvalId: request.approvalId,
439
+ ...request.sessionId !== void 0 ? { sessionId: request.sessionId } : {},
440
+ ...response
441
+ };
442
+ }
443
+ getElicitationFormData(response) {
444
+ if (response.data && typeof response.data === "object" && "formData" in response.data && typeof response.data.formData === "object" && response.data.formData !== null) {
445
+ return response.data.formData;
446
+ }
447
+ if (response.data === void 0 || typeof response.data === "object" && response.data !== null && !("formData" in response.data)) {
448
+ return {};
449
+ }
450
+ throw ApprovalError.invalidResponse("Approved elicitation response is missing formData", {
451
+ approvalId: response.approvalId,
452
+ type: ApprovalType.ELICITATION,
453
+ field: "formData"
454
+ });
279
455
  }
280
456
  /**
281
457
  * Request directory access approval.
@@ -294,16 +470,14 @@ class ApprovalManager {
294
470
  */
295
471
  async requestDirectoryAccess(metadata) {
296
472
  const { sessionId, timeout, ...directoryMetadata } = metadata;
297
- const details = {
298
- type: ApprovalType.DIRECTORY_ACCESS,
299
- // Use provided timeout, fallback to config timeout, or undefined (no timeout)
300
- timeout: timeout !== void 0 ? timeout : this.config.permissions.timeout,
301
- metadata: directoryMetadata
302
- };
303
- if (sessionId !== void 0) {
304
- details.sessionId = sessionId;
305
- }
306
- return this.requestApproval(details);
473
+ return this.requestApproval(
474
+ this.createApprovalDetails(
475
+ ApprovalType.DIRECTORY_ACCESS,
476
+ directoryMetadata,
477
+ sessionId,
478
+ timeout
479
+ )
480
+ );
307
481
  }
308
482
  /**
309
483
  * Request a generic approval
@@ -334,29 +508,19 @@ class ApprovalManager {
334
508
  this.logger.info(
335
509
  `Auto-approve approval '${request.type}', approvalId: ${request.approvalId}`
336
510
  );
337
- const response = {
338
- approvalId: request.approvalId,
511
+ return this.createResponse(request, {
339
512
  status: ApprovalStatus.APPROVED
340
- };
341
- if (request.sessionId !== void 0) {
342
- response.sessionId = request.sessionId;
343
- }
344
- return response;
513
+ });
345
514
  }
346
515
  if (mode === "auto-deny") {
347
516
  this.logger.info(
348
517
  `Auto-deny approval '${request.type}', approvalId: ${request.approvalId}`
349
518
  );
350
- const response = {
351
- approvalId: request.approvalId,
519
+ return this.createResponse(request, {
352
520
  status: ApprovalStatus.DENIED,
353
521
  reason: DenialReason.SYSTEM_DENIED,
354
522
  message: `Approval automatically denied by system policy (auto-deny mode)`
355
- };
356
- if (request.sessionId !== void 0) {
357
- response.sessionId = request.sessionId;
358
- }
359
- return response;
523
+ });
360
524
  }
361
525
  const handler = this.ensureHandler();
362
526
  this.logger.info(
@@ -373,16 +537,9 @@ class ApprovalManager {
373
537
  */
374
538
  async requestToolApproval(metadata) {
375
539
  const { sessionId, timeout, ...toolMetadata } = metadata;
376
- const details = {
377
- type: ApprovalType.TOOL_APPROVAL,
378
- // Use provided timeout, fallback to config timeout, or undefined (no timeout)
379
- timeout: timeout !== void 0 ? timeout : this.config.permissions.timeout,
380
- metadata: toolMetadata
381
- };
382
- if (sessionId !== void 0) {
383
- details.sessionId = sessionId;
384
- }
385
- return this.requestApproval(details);
540
+ return this.requestApproval(
541
+ this.createApprovalDetails(ApprovalType.TOOL_APPROVAL, toolMetadata, sessionId, timeout)
542
+ );
386
543
  }
387
544
  /**
388
545
  * Request command confirmation approval
@@ -407,16 +564,14 @@ class ApprovalManager {
407
564
  */
408
565
  async requestCommandConfirmation(metadata) {
409
566
  const { sessionId, timeout, ...commandMetadata } = metadata;
410
- const details = {
411
- type: ApprovalType.COMMAND_CONFIRMATION,
412
- // Use provided timeout, fallback to config timeout, or undefined (no timeout)
413
- timeout: timeout !== void 0 ? timeout : this.config.permissions.timeout,
414
- metadata: commandMetadata
415
- };
416
- if (sessionId !== void 0) {
417
- details.sessionId = sessionId;
418
- }
419
- return this.requestApproval(details);
567
+ return this.requestApproval(
568
+ this.createApprovalDetails(
569
+ ApprovalType.COMMAND_CONFIRMATION,
570
+ commandMetadata,
571
+ sessionId,
572
+ timeout
573
+ )
574
+ );
420
575
  }
421
576
  /**
422
577
  * Request elicitation from MCP server
@@ -427,16 +582,14 @@ class ApprovalManager {
427
582
  */
428
583
  async requestElicitation(metadata) {
429
584
  const { sessionId, timeout, ...elicitationMetadata } = metadata;
430
- const details = {
431
- type: ApprovalType.ELICITATION,
432
- // Use provided timeout, fallback to config timeout, or undefined (no timeout)
433
- timeout: timeout !== void 0 ? timeout : this.config.elicitation.timeout,
434
- metadata: elicitationMetadata
435
- };
436
- if (sessionId !== void 0) {
437
- details.sessionId = sessionId;
438
- }
439
- return this.requestApproval(details);
585
+ return this.requestApproval(
586
+ this.createApprovalDetails(
587
+ ApprovalType.ELICITATION,
588
+ elicitationMetadata,
589
+ sessionId,
590
+ timeout
591
+ )
592
+ );
440
593
  }
441
594
  /**
442
595
  * Check if tool approval was approved
@@ -468,10 +621,7 @@ class ApprovalManager {
468
621
  async getElicitationData(metadata) {
469
622
  const response = await this.requestElicitation(metadata);
470
623
  if (response.status === ApprovalStatus.APPROVED) {
471
- if (response.data && typeof response.data === "object" && "formData" in response.data && typeof response.data.formData === "object" && response.data.formData !== null) {
472
- return response.data.formData;
473
- }
474
- return {};
624
+ return this.getElicitationFormData(response);
475
625
  } else if (response.status === ApprovalStatus.DENIED) {
476
626
  throw ApprovalError.elicitationDenied(
477
627
  metadata.serverName,