@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.
- package/dist/agent/DextoAgent.cjs +52 -17
- package/dist/agent/DextoAgent.d.ts +11 -6
- package/dist/agent/DextoAgent.d.ts.map +1 -1
- package/dist/agent/DextoAgent.js +52 -17
- package/dist/agent/state-manager.cjs +6 -0
- package/dist/agent/state-manager.d.ts +4 -0
- package/dist/agent/state-manager.d.ts.map +1 -1
- package/dist/agent/state-manager.js +6 -0
- package/dist/approval/manager.cjs +328 -178
- package/dist/approval/manager.d.ts +39 -31
- package/dist/approval/manager.d.ts.map +1 -1
- package/dist/approval/manager.js +328 -178
- package/dist/approval/session-approval-store.cjs +91 -0
- package/dist/approval/session-approval-store.d.ts +55 -0
- package/dist/approval/session-approval-store.d.ts.map +1 -0
- package/dist/approval/session-approval-store.js +68 -0
- package/dist/llm/executor/stream-processor.cjs +24 -15
- package/dist/llm/executor/stream-processor.d.ts +5 -0
- package/dist/llm/executor/stream-processor.d.ts.map +1 -1
- package/dist/llm/executor/stream-processor.js +24 -15
- package/dist/llm/executor/turn-executor.cjs +7 -3
- package/dist/llm/executor/turn-executor.d.ts.map +1 -1
- package/dist/llm/executor/turn-executor.js +7 -3
- package/dist/llm/services/factory.cjs +10 -4
- package/dist/llm/services/factory.d.ts +2 -21
- package/dist/llm/services/factory.d.ts.map +1 -1
- package/dist/llm/services/factory.js +11 -7
- package/dist/llm/services/types.d.ts +33 -2
- package/dist/llm/services/types.d.ts.map +1 -1
- package/dist/llm/services/vercel.cjs +4 -5
- package/dist/llm/services/vercel.d.ts +3 -3
- package/dist/llm/services/vercel.d.ts.map +1 -1
- package/dist/llm/services/vercel.js +2 -3
- package/dist/logger/default-logger-factory.d.ts +12 -12
- package/dist/logger/v2/schemas.d.ts +6 -6
- package/dist/mcp/schemas.d.ts +10 -10
- package/dist/session/chat-session.cjs +39 -41
- package/dist/session/chat-session.d.ts +22 -12
- package/dist/session/chat-session.d.ts.map +1 -1
- package/dist/session/chat-session.js +39 -41
- package/dist/session/message-queue-store.cjs +75 -0
- package/dist/session/message-queue-store.d.ts +16 -0
- package/dist/session/message-queue-store.d.ts.map +1 -0
- package/dist/session/message-queue-store.js +52 -0
- package/dist/session/message-queue.cjs +140 -46
- package/dist/session/message-queue.d.ts +18 -6
- package/dist/session/message-queue.d.ts.map +1 -1
- package/dist/session/message-queue.js +140 -46
- package/dist/session/session-manager.cjs +130 -25
- package/dist/session/session-manager.d.ts +18 -1
- package/dist/session/session-manager.d.ts.map +1 -1
- package/dist/session/session-manager.js +130 -25
- package/dist/session/title-generator.cjs +9 -2
- package/dist/session/title-generator.d.ts +2 -0
- package/dist/session/title-generator.d.ts.map +1 -1
- package/dist/session/title-generator.js +9 -2
- package/dist/telemetry/errors.cjs +2 -2
- package/dist/telemetry/errors.js +2 -2
- package/dist/telemetry/index.d.ts +1 -1
- package/dist/telemetry/index.d.ts.map +1 -1
- package/dist/telemetry/index.js +3 -1
- package/dist/telemetry/telemetry.cjs +62 -21
- package/dist/telemetry/telemetry.d.ts +14 -0
- package/dist/telemetry/telemetry.d.ts.map +1 -1
- package/dist/telemetry/telemetry.js +62 -21
- package/dist/test-utils/session-state-stores.cjs +68 -0
- package/dist/test-utils/session-state-stores.js +42 -0
- package/dist/tools/session-tool-preferences-store.cjs +86 -0
- package/dist/tools/session-tool-preferences-store.d.ts +29 -0
- package/dist/tools/session-tool-preferences-store.d.ts.map +1 -0
- package/dist/tools/session-tool-preferences-store.js +63 -0
- package/dist/tools/tool-manager.cjs +131 -32
- package/dist/tools/tool-manager.d.ts +17 -6
- package/dist/tools/tool-manager.d.ts.map +1 -1
- package/dist/tools/tool-manager.js +131 -32
- package/dist/utils/service-initializer.cjs +38 -5
- package/dist/utils/service-initializer.d.ts +11 -1
- package/dist/utils/service-initializer.d.ts.map +1 -1
- package/dist/utils/service-initializer.js +36 -4
- package/package.json +1 -1
package/dist/approval/manager.js
CHANGED
|
@@ -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
|
-
|
|
35
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
72
|
-
this.
|
|
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
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
(
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
137
|
-
const resolved = path.resolve(
|
|
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
|
-
|
|
145
|
-
const
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
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
|
|
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
|
-
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
|
|
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.
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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
|
-
`
|
|
339
|
+
`Added approved directory in '${this.getScopeLabel(sessionId)}': "${resolvedKey}" (type: ${effectiveType})${realKey ? `, realpath: "${realKey}"` : ""}`
|
|
195
340
|
);
|
|
196
|
-
|
|
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
|
-
|
|
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
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
-
|
|
254
|
-
this.
|
|
255
|
-
|
|
256
|
-
this.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
277
|
-
this.
|
|
278
|
-
this.
|
|
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
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
377
|
-
|
|
378
|
-
|
|
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
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
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
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
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
|
-
|
|
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,
|