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