crewly 1.11.6 → 1.12.1
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/config/skills/agent/onboarding/synthesize-hierarchy/SKILL.md +65 -0
- package/config/skills/agent/onboarding/synthesize-hierarchy/execute.sh +61 -0
- package/config/skills/agent/web-search/SKILL.md +70 -0
- package/config/skills/agent/web-search/execute.sh +170 -0
- package/config/skills/agent/web-search/skill.json +23 -0
- package/dist/backend/backend/src/constants.d.ts +12 -0
- package/dist/backend/backend/src/constants.d.ts.map +1 -1
- package/dist/backend/backend/src/constants.js +12 -0
- package/dist/backend/backend/src/constants.js.map +1 -1
- package/dist/backend/backend/src/controllers/cloud/cloud.controller.d.ts +22 -0
- package/dist/backend/backend/src/controllers/cloud/cloud.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/cloud/cloud.controller.js +58 -0
- package/dist/backend/backend/src/controllers/cloud/cloud.controller.js.map +1 -1
- package/dist/backend/backend/src/controllers/cloud/cloud.routes.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/cloud/cloud.routes.js +3 -1
- package/dist/backend/backend/src/controllers/cloud/cloud.routes.js.map +1 -1
- package/dist/backend/backend/src/controllers/orchestrator-onboarding/orchestrator-onboarding.controller.d.ts +27 -0
- package/dist/backend/backend/src/controllers/orchestrator-onboarding/orchestrator-onboarding.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/orchestrator-onboarding/orchestrator-onboarding.controller.js +108 -0
- package/dist/backend/backend/src/controllers/orchestrator-onboarding/orchestrator-onboarding.controller.js.map +1 -1
- package/dist/backend/backend/src/controllers/orchestrator-onboarding/orchestrator-onboarding.routes.d.ts +6 -2
- package/dist/backend/backend/src/controllers/orchestrator-onboarding/orchestrator-onboarding.routes.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/orchestrator-onboarding/orchestrator-onboarding.routes.js +9 -3
- package/dist/backend/backend/src/controllers/orchestrator-onboarding/orchestrator-onboarding.routes.js.map +1 -1
- package/dist/backend/backend/src/index.d.ts.map +1 -1
- package/dist/backend/backend/src/index.js +36 -2
- package/dist/backend/backend/src/index.js.map +1 -1
- package/dist/backend/backend/src/services/agent/crewly-agent/crewly-agent-external-runtime.service.d.ts +18 -0
- package/dist/backend/backend/src/services/agent/crewly-agent/crewly-agent-external-runtime.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/agent/crewly-agent/crewly-agent-external-runtime.service.js +24 -2
- package/dist/backend/backend/src/services/agent/crewly-agent/crewly-agent-external-runtime.service.js.map +1 -1
- package/dist/backend/backend/src/services/cloud/mobile-api-relay.service.d.ts +102 -0
- package/dist/backend/backend/src/services/cloud/mobile-api-relay.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/cloud/mobile-api-relay.service.js +167 -0
- package/dist/backend/backend/src/services/cloud/mobile-api-relay.service.js.map +1 -0
- package/dist/backend/backend/src/services/fission/fission-guard.service.d.ts +21 -0
- package/dist/backend/backend/src/services/fission/fission-guard.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/fission/fission-guard.service.js +30 -0
- package/dist/backend/backend/src/services/fission/fission-guard.service.js.map +1 -1
- package/dist/backend/backend/src/services/intent-task/intent-classifier.rules.d.ts +4 -0
- package/dist/backend/backend/src/services/intent-task/intent-classifier.rules.d.ts.map +1 -1
- package/dist/backend/backend/src/services/intent-task/intent-classifier.rules.js +8 -0
- package/dist/backend/backend/src/services/intent-task/intent-classifier.rules.js.map +1 -1
- package/dist/backend/backend/src/services/orchestrator/onboarding/materialize-team.d.ts +79 -58
- package/dist/backend/backend/src/services/orchestrator/onboarding/materialize-team.d.ts.map +1 -1
- package/dist/backend/backend/src/services/orchestrator/onboarding/materialize-team.js +140 -65
- package/dist/backend/backend/src/services/orchestrator/onboarding/materialize-team.js.map +1 -1
- package/dist/backend/backend/src/services/orchestrator/onboarding/synthesize-hierarchy.d.ts +117 -0
- package/dist/backend/backend/src/services/orchestrator/onboarding/synthesize-hierarchy.d.ts.map +1 -0
- package/dist/backend/backend/src/services/orchestrator/onboarding/synthesize-hierarchy.js +189 -0
- package/dist/backend/backend/src/services/orchestrator/onboarding/synthesize-hierarchy.js.map +1 -0
- package/dist/backend/backend/src/services/orchestrator/onboarding-mode-loader.d.ts.map +1 -1
- package/dist/backend/backend/src/services/orchestrator/onboarding-mode-loader.js +1 -0
- package/dist/backend/backend/src/services/orchestrator/onboarding-mode-loader.js.map +1 -1
- package/dist/backend/backend/src/services/orchestrator/onboarding-mode.skill-allowlist.d.ts.map +1 -1
- package/dist/backend/backend/src/services/orchestrator/onboarding-mode.skill-allowlist.js +2 -0
- package/dist/backend/backend/src/services/orchestrator/onboarding-mode.skill-allowlist.js.map +1 -1
- package/dist/backend/backend/src/services/orchestrator/prompts/onboarding-mode.prompt.d.ts.map +1 -1
- package/dist/backend/backend/src/services/orchestrator/prompts/onboarding-mode.prompt.js +17 -1
- package/dist/backend/backend/src/services/orchestrator/prompts/onboarding-mode.prompt.js.map +1 -1
- package/dist/backend/backend/src/services/reconciler/reconcile-rules.d.ts +50 -0
- package/dist/backend/backend/src/services/reconciler/reconcile-rules.d.ts.map +1 -1
- package/dist/backend/backend/src/services/reconciler/reconcile-rules.js +71 -0
- package/dist/backend/backend/src/services/reconciler/reconcile-rules.js.map +1 -1
- package/dist/backend/backend/src/services/reconciler/reconciler.service.d.ts +18 -0
- package/dist/backend/backend/src/services/reconciler/reconciler.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/reconciler/reconciler.service.js +75 -1
- package/dist/backend/backend/src/services/reconciler/reconciler.service.js.map +1 -1
- package/dist/backend/backend/src/services/session/pty/pty-session-backend.d.ts +115 -0
- package/dist/backend/backend/src/services/session/pty/pty-session-backend.d.ts.map +1 -1
- package/dist/backend/backend/src/services/session/pty/pty-session-backend.js +189 -3
- package/dist/backend/backend/src/services/session/pty/pty-session-backend.js.map +1 -1
- package/dist/backend/backend/src/services/session/pty/pty-session.d.ts +28 -0
- package/dist/backend/backend/src/services/session/pty/pty-session.d.ts.map +1 -1
- package/dist/backend/backend/src/services/session/pty/pty-session.js +61 -1
- package/dist/backend/backend/src/services/session/pty/pty-session.js.map +1 -1
- package/dist/backend/backend/src/services/template/template.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/template/template.service.js +67 -2
- package/dist/backend/backend/src/services/template/template.service.js.map +1 -1
- package/dist/backend/backend/src/services/v3/cascade-request-status.d.ts +19 -1
- package/dist/backend/backend/src/services/v3/cascade-request-status.d.ts.map +1 -1
- package/dist/backend/backend/src/services/v3/cascade-request-status.js +39 -2
- package/dist/backend/backend/src/services/v3/cascade-request-status.js.map +1 -1
- package/dist/backend/backend/src/services/v3/escalation-router.service.d.ts +41 -0
- package/dist/backend/backend/src/services/v3/escalation-router.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/v3/escalation-router.service.js +169 -0
- package/dist/backend/backend/src/services/v3/escalation-router.service.js.map +1 -1
- package/dist/backend/backend/src/services/v3/request-cascade.subscriber.d.ts +4 -1
- package/dist/backend/backend/src/services/v3/request-cascade.subscriber.d.ts.map +1 -1
- package/dist/backend/backend/src/services/v3/request-cascade.subscriber.js +21 -0
- package/dist/backend/backend/src/services/v3/request-cascade.subscriber.js.map +1 -1
- package/dist/backend/backend/src/types/intent-task.types.d.ts.map +1 -1
- package/dist/backend/backend/src/types/intent-task.types.js +8 -0
- package/dist/backend/backend/src/types/intent-task.types.js.map +1 -1
- package/dist/backend/backend/src/types/v2/request.types.d.ts +1 -1
- package/dist/backend/backend/src/types/v2/request.types.d.ts.map +1 -1
- package/dist/backend/backend/src/types/v2/request.types.js +1 -0
- package/dist/backend/backend/src/types/v2/request.types.js.map +1 -1
- package/dist/cli/backend/src/constants.d.ts +12 -0
- package/dist/cli/backend/src/constants.d.ts.map +1 -1
- package/dist/cli/backend/src/constants.js +12 -0
- package/dist/cli/backend/src/constants.js.map +1 -1
- package/package.json +9 -3
- package/packages/crewly-agent/README.md +27 -0
- package/packages/crewly-agent/bin/crewly-agent +33 -0
- package/packages/crewly-agent/package.json +39 -0
- package/packages/crewly-agent/src/cli.ts +168 -0
- package/packages/crewly-agent/src/runtime/agent-runner.service.test.ts +2355 -0
- package/packages/crewly-agent/src/runtime/agent-runner.service.ts +1827 -0
- package/packages/crewly-agent/src/runtime/agent-stream.service.test.ts +153 -0
- package/packages/crewly-agent/src/runtime/agent-stream.service.ts +225 -0
- package/packages/crewly-agent/src/runtime/agent-worker.test.ts +171 -0
- package/packages/crewly-agent/src/runtime/agent-worker.ts +193 -0
- package/packages/crewly-agent/src/runtime/api-client.ts +143 -0
- package/packages/crewly-agent/src/runtime/approval-queue.service.ts +307 -0
- package/packages/crewly-agent/src/runtime/audit-log.service.test.ts +208 -0
- package/packages/crewly-agent/src/runtime/audit-log.service.ts +332 -0
- package/packages/crewly-agent/src/runtime/audit-trail.service.test.ts +178 -0
- package/packages/crewly-agent/src/runtime/audit-trail.service.ts +151 -0
- package/packages/crewly-agent/src/runtime/auditor-tools.test.ts +274 -0
- package/packages/crewly-agent/src/runtime/auditor-tools.ts +311 -0
- package/packages/crewly-agent/src/runtime/cloud-config.ts +67 -0
- package/packages/crewly-agent/src/runtime/deepseek-sse-transform.test.ts +165 -0
- package/packages/crewly-agent/src/runtime/deepseek-sse-transform.ts +168 -0
- package/packages/crewly-agent/src/runtime/env-isolation.service.ts +246 -0
- package/packages/crewly-agent/src/runtime/in-process-log-buffer.test.ts +280 -0
- package/packages/crewly-agent/src/runtime/in-process-log-buffer.ts +317 -0
- package/packages/crewly-agent/src/runtime/index.ts +38 -0
- package/packages/crewly-agent/src/runtime/mcp-tool-bridge.test.ts +352 -0
- package/packages/crewly-agent/src/runtime/mcp-tool-bridge.ts +244 -0
- package/packages/crewly-agent/src/runtime/model-manager.test.ts +326 -0
- package/packages/crewly-agent/src/runtime/model-manager.ts +363 -0
- package/packages/crewly-agent/src/runtime/output-filter.service.ts +175 -0
- package/packages/crewly-agent/src/runtime/prompt-guard.service.ts +303 -0
- package/packages/crewly-agent/src/runtime/rate-limiter.test.ts +228 -0
- package/packages/crewly-agent/src/runtime/rate-limiter.ts +353 -0
- package/packages/crewly-agent/src/runtime/tool-registry.test.ts +2510 -0
- package/packages/crewly-agent/src/runtime/tool-registry.ts +2104 -0
- package/packages/crewly-agent/src/runtime/types.test.ts +519 -0
- package/packages/crewly-agent/src/runtime/types.ts +637 -0
- package/packages/crewly-agent/src/runtime/web-search.tool.test.ts +131 -0
- package/packages/crewly-agent/src/runtime/web-search.tool.ts +140 -0
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-Process Log Buffer
|
|
3
|
+
*
|
|
4
|
+
* Ring buffer that captures structured output from in-process Crewly Agent
|
|
5
|
+
* runtimes. Provides the same interface as PTY terminal capture so the
|
|
6
|
+
* frontend Side Terminal panel can display crewly-agent activity.
|
|
7
|
+
*
|
|
8
|
+
* Extends EventEmitter to support real-time WebSocket streaming of log entries
|
|
9
|
+
* to the frontend Side Terminal panel.
|
|
10
|
+
*
|
|
11
|
+
* Also persists log entries to disk at ~/.crewly/logs/sessions/{sessionName}.log,
|
|
12
|
+
* matching the file-based logging that PTY-based agents get via PtySessionBackend.
|
|
13
|
+
*
|
|
14
|
+
* @module services/agent/crewly-agent/in-process-log-buffer
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { EventEmitter } from 'events';
|
|
18
|
+
import * as fs from 'fs';
|
|
19
|
+
import * as path from 'path';
|
|
20
|
+
import * as os from 'os';
|
|
21
|
+
/**
|
|
22
|
+
* Local copies of the OSS constants this module relies on — standalone
|
|
23
|
+
* crewly-agent must not depend on `crewly/backend/src/constants.ts`. Keep
|
|
24
|
+
* these path fragments in sync with the values OSS writes elsewhere (any
|
|
25
|
+
* code that reads back the same directory tree on disk).
|
|
26
|
+
*/
|
|
27
|
+
const CREWLY_CONSTANTS = {
|
|
28
|
+
PATHS: {
|
|
29
|
+
CREWLY_HOME: '.crewly',
|
|
30
|
+
LOGS_DIR: 'logs',
|
|
31
|
+
},
|
|
32
|
+
} as const;
|
|
33
|
+
|
|
34
|
+
const LOG_ROTATION_CONSTANTS = {
|
|
35
|
+
SESSIONS_LOG_DIR: 'sessions',
|
|
36
|
+
} as const;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Single log entry from an in-process agent session.
|
|
40
|
+
*/
|
|
41
|
+
export interface LogEntry {
|
|
42
|
+
/** ISO timestamp */
|
|
43
|
+
timestamp: string;
|
|
44
|
+
/** Log level */
|
|
45
|
+
level: 'info' | 'debug' | 'warn' | 'error';
|
|
46
|
+
/** Log message */
|
|
47
|
+
message: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Maximum number of entries to keep per session */
|
|
51
|
+
const MAX_ENTRIES_PER_SESSION = 500;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Singleton buffer that captures in-process agent output.
|
|
55
|
+
*
|
|
56
|
+
* Used by CrewlyAgentRuntimeService to log tool calls, responses,
|
|
57
|
+
* and errors. The terminal controller reads from this buffer when
|
|
58
|
+
* the requested session is an in-process agent (no PTY).
|
|
59
|
+
*
|
|
60
|
+
* Emits 'data' events with (sessionName, formattedLine) when new entries
|
|
61
|
+
* are appended, enabling real-time WebSocket streaming via TerminalGateway.
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* ```typescript
|
|
65
|
+
* const buffer = InProcessLogBuffer.getInstance();
|
|
66
|
+
* buffer.append('crewly-assistant', 'info', 'Calling get_team_status tool...');
|
|
67
|
+
* const output = buffer.capture('crewly-assistant', 50);
|
|
68
|
+
*
|
|
69
|
+
* // Real-time streaming
|
|
70
|
+
* buffer.on('data', (sessionName, formattedLine) => {
|
|
71
|
+
* console.log(`[${sessionName}] ${formattedLine}`);
|
|
72
|
+
* });
|
|
73
|
+
* ```
|
|
74
|
+
*/
|
|
75
|
+
export class InProcessLogBuffer extends EventEmitter {
|
|
76
|
+
private static instance: InProcessLogBuffer | null = null;
|
|
77
|
+
private sessions = new Map<string, LogEntry[]>();
|
|
78
|
+
/** File write streams for persisting logs to disk */
|
|
79
|
+
private logStreams = new Map<string, fs.WriteStream>();
|
|
80
|
+
/** Resolved path to ~/.crewly/logs/sessions/ */
|
|
81
|
+
private sessionLogsDir: string;
|
|
82
|
+
|
|
83
|
+
constructor() {
|
|
84
|
+
super();
|
|
85
|
+
this.sessionLogsDir = path.join(
|
|
86
|
+
os.homedir(),
|
|
87
|
+
CREWLY_CONSTANTS.PATHS.CREWLY_HOME,
|
|
88
|
+
CREWLY_CONSTANTS.PATHS.LOGS_DIR,
|
|
89
|
+
LOG_ROTATION_CONSTANTS.SESSIONS_LOG_DIR,
|
|
90
|
+
);
|
|
91
|
+
// Ensure directory exists (non-fatal if it fails)
|
|
92
|
+
try {
|
|
93
|
+
fs.mkdirSync(this.sessionLogsDir, { recursive: true });
|
|
94
|
+
} catch {
|
|
95
|
+
// Directory may already exist or be uncreatable — logging will be skipped
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Get the singleton instance.
|
|
101
|
+
*
|
|
102
|
+
* @returns The shared InProcessLogBuffer instance
|
|
103
|
+
*/
|
|
104
|
+
static getInstance(): InProcessLogBuffer {
|
|
105
|
+
if (!InProcessLogBuffer.instance) {
|
|
106
|
+
InProcessLogBuffer.instance = new InProcessLogBuffer();
|
|
107
|
+
}
|
|
108
|
+
return InProcessLogBuffer.instance;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Reset the singleton (for testing).
|
|
113
|
+
*/
|
|
114
|
+
static resetInstance(): void {
|
|
115
|
+
if (InProcessLogBuffer.instance) {
|
|
116
|
+
// Close all open file streams
|
|
117
|
+
for (const stream of InProcessLogBuffer.instance.logStreams.values()) {
|
|
118
|
+
try { stream.end(); } catch { /* ignore */ }
|
|
119
|
+
}
|
|
120
|
+
InProcessLogBuffer.instance.logStreams.clear();
|
|
121
|
+
InProcessLogBuffer.instance.removeAllListeners();
|
|
122
|
+
}
|
|
123
|
+
InProcessLogBuffer.instance = null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Format a log entry into a single display line.
|
|
128
|
+
*
|
|
129
|
+
* @param entry - The log entry to format
|
|
130
|
+
* @returns Formatted string like "[HH:MM:SS.mmm] ERROR: message"
|
|
131
|
+
*/
|
|
132
|
+
private formatEntry(entry: LogEntry): string {
|
|
133
|
+
const ts = entry.timestamp.substring(11, 23); // HH:MM:SS.mmm
|
|
134
|
+
const prefix = entry.level === 'error' ? 'ERROR' : entry.level === 'warn' ? 'WARN' : entry.level === 'debug' ? 'DEBUG' : '';
|
|
135
|
+
return prefix ? `[${ts}] ${prefix}: ${entry.message}` : `[${ts}] ${entry.message}`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Append a log entry for a session.
|
|
140
|
+
*
|
|
141
|
+
* Emits a 'data' event with the session name and formatted line for
|
|
142
|
+
* real-time WebSocket streaming.
|
|
143
|
+
*
|
|
144
|
+
* @param sessionName - In-process agent session name
|
|
145
|
+
* @param level - Log level
|
|
146
|
+
* @param message - Log message text
|
|
147
|
+
*/
|
|
148
|
+
append(sessionName: string, level: LogEntry['level'], message: string): void {
|
|
149
|
+
if (!this.sessions.has(sessionName)) {
|
|
150
|
+
this.sessions.set(sessionName, []);
|
|
151
|
+
}
|
|
152
|
+
const entries = this.sessions.get(sessionName)!;
|
|
153
|
+
const entry: LogEntry = {
|
|
154
|
+
timestamp: new Date().toISOString(),
|
|
155
|
+
level,
|
|
156
|
+
message,
|
|
157
|
+
};
|
|
158
|
+
entries.push(entry);
|
|
159
|
+
// Ring buffer — drop oldest entries
|
|
160
|
+
if (entries.length > MAX_ENTRIES_PER_SESSION) {
|
|
161
|
+
entries.splice(0, entries.length - MAX_ENTRIES_PER_SESSION);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Emit for real-time WebSocket streaming
|
|
165
|
+
const formattedLine = this.formatEntry(entry);
|
|
166
|
+
this.emit('data', sessionName, formattedLine);
|
|
167
|
+
|
|
168
|
+
// Persist to disk (fire-and-forget, non-blocking)
|
|
169
|
+
this.writeToFile(sessionName, entry, formattedLine);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Check if a session has any log entries.
|
|
174
|
+
*
|
|
175
|
+
* @param sessionName - Session to check
|
|
176
|
+
* @returns True if the session has been registered with at least one entry
|
|
177
|
+
*/
|
|
178
|
+
hasSession(sessionName: string): boolean {
|
|
179
|
+
return this.sessions.has(sessionName);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Capture recent output from an in-process session, formatted like
|
|
184
|
+
* terminal output for frontend compatibility.
|
|
185
|
+
*
|
|
186
|
+
* @param sessionName - Session to capture from
|
|
187
|
+
* @param lines - Maximum number of lines to return
|
|
188
|
+
* @returns Formatted output string (one log entry per line)
|
|
189
|
+
*/
|
|
190
|
+
capture(sessionName: string, lines = 100): string {
|
|
191
|
+
const entries = this.sessions.get(sessionName);
|
|
192
|
+
if (!entries || entries.length === 0) {
|
|
193
|
+
return '[crewly-agent] No output yet';
|
|
194
|
+
}
|
|
195
|
+
const slice = entries.slice(-lines);
|
|
196
|
+
return slice.map(e => this.formatEntry(e)).join('\n');
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Register a session (creates empty entry list and opens a file log stream).
|
|
201
|
+
*
|
|
202
|
+
* @param sessionName - Session to register
|
|
203
|
+
*/
|
|
204
|
+
registerSession(sessionName: string): void {
|
|
205
|
+
if (!this.sessions.has(sessionName)) {
|
|
206
|
+
this.sessions.set(sessionName, []);
|
|
207
|
+
}
|
|
208
|
+
this.openLogStream(sessionName);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Remove a session's log buffer and close its file stream.
|
|
213
|
+
* The log file is preserved on disk for post-mortem analysis.
|
|
214
|
+
*
|
|
215
|
+
* @param sessionName - Session to remove
|
|
216
|
+
*/
|
|
217
|
+
removeSession(sessionName: string): void {
|
|
218
|
+
this.sessions.delete(sessionName);
|
|
219
|
+
this.closeLogStream(sessionName);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Get all registered in-process session names.
|
|
224
|
+
*
|
|
225
|
+
* @returns Array of session names
|
|
226
|
+
*/
|
|
227
|
+
getSessionNames(): string[] {
|
|
228
|
+
return Array.from(this.sessions.keys());
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Clear all sessions and close all file streams (for testing).
|
|
233
|
+
*/
|
|
234
|
+
clear(): void {
|
|
235
|
+
this.sessions.clear();
|
|
236
|
+
for (const stream of this.logStreams.values()) {
|
|
237
|
+
try { stream.end(); } catch { /* ignore */ }
|
|
238
|
+
}
|
|
239
|
+
this.logStreams.clear();
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// ===== Private file I/O helpers =====
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Get the file path for a session's persistent log.
|
|
246
|
+
*
|
|
247
|
+
* @param sessionName - Session name
|
|
248
|
+
* @returns Absolute path to the log file
|
|
249
|
+
*/
|
|
250
|
+
private getLogPath(sessionName: string): string {
|
|
251
|
+
return path.join(this.sessionLogsDir, `${sessionName}.log`);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Open a writable file stream for a session log.
|
|
256
|
+
* Appends a session marker so multiple runs are distinguishable.
|
|
257
|
+
*
|
|
258
|
+
* @param sessionName - Session name
|
|
259
|
+
*/
|
|
260
|
+
private openLogStream(sessionName: string): void {
|
|
261
|
+
// Don't open duplicate streams
|
|
262
|
+
if (this.logStreams.has(sessionName)) return;
|
|
263
|
+
|
|
264
|
+
try {
|
|
265
|
+
const logPath = this.getLogPath(sessionName);
|
|
266
|
+
const fileExists = fs.existsSync(logPath);
|
|
267
|
+
|
|
268
|
+
const stream = fs.createWriteStream(logPath, { flags: 'a' });
|
|
269
|
+
stream.on('error', () => {
|
|
270
|
+
// Silently remove broken streams — disk logging is best-effort
|
|
271
|
+
this.logStreams.delete(sessionName);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
const marker = fileExists ? 'RESTARTED' : 'STARTED';
|
|
275
|
+
stream.write(`\n--- SESSION ${marker} at ${new Date().toISOString()} ---\n\n`);
|
|
276
|
+
|
|
277
|
+
this.logStreams.set(sessionName, stream);
|
|
278
|
+
} catch {
|
|
279
|
+
// Non-fatal — in-memory buffer and WebSocket streaming still work
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Close and remove the file stream for a session.
|
|
285
|
+
*
|
|
286
|
+
* @param sessionName - Session name
|
|
287
|
+
*/
|
|
288
|
+
private closeLogStream(sessionName: string): void {
|
|
289
|
+
const stream = this.logStreams.get(sessionName);
|
|
290
|
+
if (stream) {
|
|
291
|
+
try {
|
|
292
|
+
stream.write(`\n--- SESSION ENDED at ${new Date().toISOString()} ---\n`);
|
|
293
|
+
stream.end();
|
|
294
|
+
} catch { /* ignore */ }
|
|
295
|
+
this.logStreams.delete(sessionName);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Write a formatted log entry to the session's file stream.
|
|
301
|
+
* Non-blocking, fire-and-forget — errors are silently ignored.
|
|
302
|
+
*
|
|
303
|
+
* @param sessionName - Session name
|
|
304
|
+
* @param entry - The log entry (for full timestamp in file)
|
|
305
|
+
* @param formattedLine - Pre-formatted display line
|
|
306
|
+
*/
|
|
307
|
+
private writeToFile(sessionName: string, entry: LogEntry, formattedLine: string): void {
|
|
308
|
+
const stream = this.logStreams.get(sessionName);
|
|
309
|
+
if (!stream) return;
|
|
310
|
+
|
|
311
|
+
try {
|
|
312
|
+
stream.write(`${entry.timestamp} ${formattedLine}\n`);
|
|
313
|
+
} catch {
|
|
314
|
+
// Non-fatal — disk logging is best-effort
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Crewly Agent Runtime — Barrel Export
|
|
3
|
+
*
|
|
4
|
+
* Standalone runtime module. The OSS-side `CrewlyAgentRuntimeService`
|
|
5
|
+
* (in-process variant) intentionally does not exist here — this package
|
|
6
|
+
* IS the process; OSS spawns it as a subprocess and talks via the JSON
|
|
7
|
+
* protocol implemented in `cli.ts`.
|
|
8
|
+
*
|
|
9
|
+
* @module runtime
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export { AgentRunnerService } from './agent-runner.service.js';
|
|
13
|
+
export { CrewlyApiClient } from './api-client.js';
|
|
14
|
+
export { ModelManager } from './model-manager.js';
|
|
15
|
+
export { InProcessLogBuffer, type LogEntry } from './in-process-log-buffer.js';
|
|
16
|
+
export { createTools, getToolNames } from './tool-registry.js';
|
|
17
|
+
export { createAuditorTools, getAuditorToolNames } from './auditor-tools.js';
|
|
18
|
+
export { AuditTrailService } from './audit-trail.service.js';
|
|
19
|
+
export { RateLimiter, RATE_LIMITER_DEFAULTS, type RateLimiterConfig } from './rate-limiter.js';
|
|
20
|
+
export { createWebSearchTool, formatAsMarkdown } from './web-search.tool.js';
|
|
21
|
+
export { loadCloudConfig, CloudNotLoggedInError, type CloudConfig } from './cloud-config.js';
|
|
22
|
+
export {
|
|
23
|
+
type ModelProvider,
|
|
24
|
+
type ModelConfig,
|
|
25
|
+
type ConversationState,
|
|
26
|
+
type CrewlyAgentConfig,
|
|
27
|
+
type AgentRunResult,
|
|
28
|
+
type ToolCallRecord,
|
|
29
|
+
type ApiCallResult,
|
|
30
|
+
type AuditEntry,
|
|
31
|
+
type SecurityPolicy,
|
|
32
|
+
type AuditLogFilters,
|
|
33
|
+
MODEL_PROVIDERS,
|
|
34
|
+
CREWLY_AGENT_DEFAULTS,
|
|
35
|
+
WRITE_TOOLS,
|
|
36
|
+
isModelProvider,
|
|
37
|
+
isModelConfig,
|
|
38
|
+
} from './types.js';
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for MCP Tool Bridge
|
|
3
|
+
*
|
|
4
|
+
* Validates MCP tool conversion, naming, sensitivity classification,
|
|
5
|
+
* tool loading, and end-to-end execution via a mock MCP server.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, it, expect, vi, type Mocked, type MockInstance } from 'vitest';
|
|
9
|
+
import {
|
|
10
|
+
MCP_TOOL_PREFIX,
|
|
11
|
+
MCP_DEFAULT_SENSITIVITY,
|
|
12
|
+
jsonSchemaToZodPassthrough,
|
|
13
|
+
buildMcpToolName,
|
|
14
|
+
resolveSensitivity,
|
|
15
|
+
convertMcpTool,
|
|
16
|
+
loadMcpTools,
|
|
17
|
+
connectAndLoadMcpTools,
|
|
18
|
+
} from './mcp-tool-bridge.js';
|
|
19
|
+
import type { McpClientService, McpToolInfo, McpServerConfig } from '../../mcp-client.js';
|
|
20
|
+
import type { ToolSensitivity } from './types.js';
|
|
21
|
+
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// Mock MCP Client
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Create a mock McpClientService with configurable tool lists.
|
|
28
|
+
*/
|
|
29
|
+
function createMockMcpClient(tools: McpToolInfo[] = []): McpClientService {
|
|
30
|
+
return {
|
|
31
|
+
connectServer: vi.fn<any>().mockResolvedValue(undefined),
|
|
32
|
+
disconnectServer: vi.fn<any>().mockResolvedValue(undefined),
|
|
33
|
+
disconnectAll: vi.fn<any>().mockResolvedValue(undefined),
|
|
34
|
+
listTools: vi.fn<any>().mockReturnValue(tools),
|
|
35
|
+
callTool: vi.fn<any>().mockResolvedValue({
|
|
36
|
+
content: [{ type: 'text', text: 'mock result' }],
|
|
37
|
+
isError: false,
|
|
38
|
+
}),
|
|
39
|
+
refreshTools: vi.fn<any>().mockResolvedValue(undefined),
|
|
40
|
+
getConnectedServers: vi.fn<any>().mockReturnValue(
|
|
41
|
+
[...new Set(tools.map(t => t.serverName))],
|
|
42
|
+
),
|
|
43
|
+
isServerConnected: vi.fn<any>().mockReturnValue(true),
|
|
44
|
+
getServerStatuses: vi.fn<any>().mockReturnValue([]),
|
|
45
|
+
connectAll: vi.fn<any>().mockResolvedValue(new Map()),
|
|
46
|
+
} as unknown as McpClientService;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Sample MCP tool info for a filesystem read tool */
|
|
50
|
+
const SAMPLE_TOOL: McpToolInfo = {
|
|
51
|
+
name: 'read_file',
|
|
52
|
+
description: 'Read the contents of a file',
|
|
53
|
+
inputSchema: {
|
|
54
|
+
type: 'object',
|
|
55
|
+
properties: {
|
|
56
|
+
path: { type: 'string', description: 'File path to read' },
|
|
57
|
+
},
|
|
58
|
+
required: ['path'],
|
|
59
|
+
},
|
|
60
|
+
serverName: 'filesystem',
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/** Sample MCP tool info without description */
|
|
64
|
+
const TOOL_NO_DESC: McpToolInfo = {
|
|
65
|
+
name: 'list_dir',
|
|
66
|
+
inputSchema: { type: 'object', properties: {} },
|
|
67
|
+
serverName: 'filesystem',
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
// Tests
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
|
|
74
|
+
describe('MCP Tool Bridge', () => {
|
|
75
|
+
describe('Constants', () => {
|
|
76
|
+
it('should have correct prefix', () => {
|
|
77
|
+
expect(MCP_TOOL_PREFIX).toBe('mcp_');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should default to sensitive classification', () => {
|
|
81
|
+
expect(MCP_DEFAULT_SENSITIVITY).toBe('sensitive');
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe('buildMcpToolName', () => {
|
|
86
|
+
it('should namespace tool names with server prefix', () => {
|
|
87
|
+
expect(buildMcpToolName('filesystem', 'read_file')).toBe('mcp_filesystem_read_file');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('should handle different server names', () => {
|
|
91
|
+
expect(buildMcpToolName('github', 'create_issue')).toBe('mcp_github_create_issue');
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe('jsonSchemaToZodPassthrough', () => {
|
|
96
|
+
it('should create a schema from JSON Schema with properties', () => {
|
|
97
|
+
const schema = jsonSchemaToZodPassthrough({
|
|
98
|
+
type: 'object',
|
|
99
|
+
properties: {
|
|
100
|
+
path: { type: 'string', description: 'File path' },
|
|
101
|
+
encoding: { type: 'string' },
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
// Should parse without errors
|
|
105
|
+
const result = schema.safeParse({ path: '/tmp/test.txt', encoding: 'utf-8' });
|
|
106
|
+
expect(result.success).toBe(true);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('should accept extra properties via passthrough', () => {
|
|
110
|
+
const schema = jsonSchemaToZodPassthrough({
|
|
111
|
+
type: 'object',
|
|
112
|
+
properties: {
|
|
113
|
+
path: { type: 'string', description: 'File path' },
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
const result = schema.safeParse({ path: '/tmp', extraKey: 'value' });
|
|
117
|
+
expect(result.success).toBe(true);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('should fall back to record schema when no properties', () => {
|
|
121
|
+
const schema = jsonSchemaToZodPassthrough({ type: 'object' });
|
|
122
|
+
const result = schema.safeParse({ anything: 'goes' });
|
|
123
|
+
expect(result.success).toBe(true);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('should handle empty properties object', () => {
|
|
127
|
+
const schema = jsonSchemaToZodPassthrough({
|
|
128
|
+
type: 'object',
|
|
129
|
+
properties: {},
|
|
130
|
+
});
|
|
131
|
+
const result = schema.safeParse({});
|
|
132
|
+
expect(result.success).toBe(true);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe('resolveSensitivity', () => {
|
|
137
|
+
it('should return default sensitivity when no overrides', () => {
|
|
138
|
+
expect(resolveSensitivity('fs', 'read', undefined)).toBe('sensitive');
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('should return default sensitivity when no matching override', () => {
|
|
142
|
+
const overrides = { 'other:tool': 'safe' as ToolSensitivity };
|
|
143
|
+
expect(resolveSensitivity('fs', 'read', overrides)).toBe('sensitive');
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('should match server-specific override', () => {
|
|
147
|
+
const overrides = { 'fs:read': 'safe' as ToolSensitivity };
|
|
148
|
+
expect(resolveSensitivity('fs', 'read', overrides)).toBe('safe');
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('should match tool-level override', () => {
|
|
152
|
+
const overrides = { read: 'safe' as ToolSensitivity };
|
|
153
|
+
expect(resolveSensitivity('fs', 'read', overrides)).toBe('safe');
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('should prefer server-specific over tool-level override', () => {
|
|
157
|
+
const overrides = {
|
|
158
|
+
'fs:read': 'destructive' as ToolSensitivity,
|
|
159
|
+
read: 'safe' as ToolSensitivity,
|
|
160
|
+
};
|
|
161
|
+
expect(resolveSensitivity('fs', 'read', overrides)).toBe('destructive');
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
describe('convertMcpTool', () => {
|
|
166
|
+
it('should convert MCP tool info to ToolDefinition', () => {
|
|
167
|
+
const mcpClient = createMockMcpClient();
|
|
168
|
+
const tool = convertMcpTool(mcpClient, SAMPLE_TOOL);
|
|
169
|
+
|
|
170
|
+
expect(tool.description).toContain('[MCP:filesystem]');
|
|
171
|
+
expect(tool.description).toContain('Read the contents of a file');
|
|
172
|
+
expect(tool.sensitivity).toBe('sensitive');
|
|
173
|
+
expect(typeof tool.execute).toBe('function');
|
|
174
|
+
expect(tool.inputSchema).toBeDefined();
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('should use tool name as description fallback', () => {
|
|
178
|
+
const mcpClient = createMockMcpClient();
|
|
179
|
+
const tool = convertMcpTool(mcpClient, TOOL_NO_DESC);
|
|
180
|
+
|
|
181
|
+
expect(tool.description).toBe('[MCP:filesystem] list_dir');
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('should apply sensitivity overrides', () => {
|
|
185
|
+
const mcpClient = createMockMcpClient();
|
|
186
|
+
const tool = convertMcpTool(mcpClient, SAMPLE_TOOL, {
|
|
187
|
+
'filesystem:read_file': 'safe',
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
expect(tool.sensitivity).toBe('safe');
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('should call mcpClient.callTool on execute', async () => {
|
|
194
|
+
const mcpClient = createMockMcpClient();
|
|
195
|
+
const tool = convertMcpTool(mcpClient, SAMPLE_TOOL);
|
|
196
|
+
|
|
197
|
+
const result = await tool.execute({ path: '/tmp/test.txt' });
|
|
198
|
+
|
|
199
|
+
expect(mcpClient.callTool).toHaveBeenCalledWith(
|
|
200
|
+
'filesystem',
|
|
201
|
+
'read_file',
|
|
202
|
+
{ path: '/tmp/test.txt' },
|
|
203
|
+
);
|
|
204
|
+
// Single text content should be flattened
|
|
205
|
+
expect(result).toEqual({ success: true, text: 'mock result' });
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('should return full content for multi-block results', async () => {
|
|
209
|
+
const mcpClient = createMockMcpClient();
|
|
210
|
+
(mcpClient.callTool as MockedFunction<any>).mockResolvedValue({
|
|
211
|
+
content: [
|
|
212
|
+
{ type: 'text', text: 'line 1' },
|
|
213
|
+
{ type: 'text', text: 'line 2' },
|
|
214
|
+
],
|
|
215
|
+
isError: false,
|
|
216
|
+
});
|
|
217
|
+
const tool = convertMcpTool(mcpClient, SAMPLE_TOOL);
|
|
218
|
+
|
|
219
|
+
const result = await tool.execute({ path: '/tmp/test.txt' }) as Record<string, unknown>;
|
|
220
|
+
|
|
221
|
+
expect(result.success).toBe(true);
|
|
222
|
+
expect(result.content).toHaveLength(2);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('should handle error results from MCP server', async () => {
|
|
226
|
+
const mcpClient = createMockMcpClient();
|
|
227
|
+
(mcpClient.callTool as MockedFunction<any>).mockResolvedValue({
|
|
228
|
+
content: [{ type: 'text', text: 'Permission denied' }],
|
|
229
|
+
isError: true,
|
|
230
|
+
});
|
|
231
|
+
const tool = convertMcpTool(mcpClient, SAMPLE_TOOL);
|
|
232
|
+
|
|
233
|
+
const result = await tool.execute({ path: '/root/secret' }) as Record<string, unknown>;
|
|
234
|
+
|
|
235
|
+
expect(result.success).toBe(false);
|
|
236
|
+
expect(result.error).toBe('MCP tool returned an error');
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('should return error result when callTool throws', async () => {
|
|
240
|
+
const mcpClient = createMockMcpClient();
|
|
241
|
+
(mcpClient.callTool as MockedFunction<any>).mockRejectedValue(new Error('Connection lost'));
|
|
242
|
+
const tool = convertMcpTool(mcpClient, SAMPLE_TOOL);
|
|
243
|
+
|
|
244
|
+
const result = await tool.execute({ path: '/tmp' }) as { success: boolean; error: string };
|
|
245
|
+
expect(result.success).toBe(false);
|
|
246
|
+
expect(result.error).toContain('Connection lost');
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
describe('loadMcpTools', () => {
|
|
251
|
+
it('should load tools from all connected servers', () => {
|
|
252
|
+
const tools: McpToolInfo[] = [
|
|
253
|
+
SAMPLE_TOOL,
|
|
254
|
+
{ name: 'write_file', description: 'Write a file', inputSchema: { type: 'object', properties: {} }, serverName: 'filesystem' },
|
|
255
|
+
{ name: 'create_issue', description: 'Create GitHub issue', inputSchema: { type: 'object', properties: {} }, serverName: 'github' },
|
|
256
|
+
];
|
|
257
|
+
const mcpClient = createMockMcpClient(tools);
|
|
258
|
+
|
|
259
|
+
const result = loadMcpTools(mcpClient);
|
|
260
|
+
|
|
261
|
+
expect(Object.keys(result)).toEqual([
|
|
262
|
+
'mcp_filesystem_read_file',
|
|
263
|
+
'mcp_filesystem_write_file',
|
|
264
|
+
'mcp_github_create_issue',
|
|
265
|
+
]);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it('should return empty map when no tools available', () => {
|
|
269
|
+
const mcpClient = createMockMcpClient([]);
|
|
270
|
+
const result = loadMcpTools(mcpClient);
|
|
271
|
+
expect(Object.keys(result)).toHaveLength(0);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it('should apply sensitivity overrides to loaded tools', () => {
|
|
275
|
+
const mcpClient = createMockMcpClient([SAMPLE_TOOL]);
|
|
276
|
+
const result = loadMcpTools(mcpClient, {
|
|
277
|
+
'filesystem:read_file': 'safe',
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
expect(result['mcp_filesystem_read_file'].sensitivity).toBe('safe');
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it('should default all tools to sensitive classification', () => {
|
|
284
|
+
const mcpClient = createMockMcpClient([SAMPLE_TOOL, TOOL_NO_DESC]);
|
|
285
|
+
const result = loadMcpTools(mcpClient);
|
|
286
|
+
|
|
287
|
+
for (const tool of Object.values(result)) {
|
|
288
|
+
expect(tool.sensitivity).toBe('sensitive');
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
describe('connectAndLoadMcpTools', () => {
|
|
294
|
+
it('should connect to servers and load tools', async () => {
|
|
295
|
+
const tools: McpToolInfo[] = [SAMPLE_TOOL];
|
|
296
|
+
const mcpClient = createMockMcpClient(tools);
|
|
297
|
+
const configs: Record<string, McpServerConfig> = {
|
|
298
|
+
filesystem: { command: 'npx', args: ['-y', '@anthropic/mcp-filesystem'] },
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
const { tools: loaded, errors } = await connectAndLoadMcpTools(mcpClient, configs);
|
|
302
|
+
|
|
303
|
+
expect(mcpClient.connectAll).toHaveBeenCalledWith(configs);
|
|
304
|
+
expect(Object.keys(loaded)).toContain('mcp_filesystem_read_file');
|
|
305
|
+
expect(errors.size).toBe(0);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it('should return errors for failed connections', async () => {
|
|
309
|
+
const mcpClient = createMockMcpClient([]);
|
|
310
|
+
const failError = new Error('spawn ENOENT');
|
|
311
|
+
(mcpClient.connectAll as MockedFunction<any>).mockResolvedValue(
|
|
312
|
+
new Map([['badserver', failError]]),
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
const { errors } = await connectAndLoadMcpTools(mcpClient, {
|
|
316
|
+
badserver: { command: 'nonexistent' },
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
expect(errors.size).toBe(1);
|
|
320
|
+
expect(errors.get('badserver')?.message).toBe('spawn ENOENT');
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it('should still load tools from servers that connected successfully', async () => {
|
|
324
|
+
const tools: McpToolInfo[] = [SAMPLE_TOOL];
|
|
325
|
+
const mcpClient = createMockMcpClient(tools);
|
|
326
|
+
(mcpClient.connectAll as MockedFunction<any>).mockResolvedValue(
|
|
327
|
+
new Map([['badserver', new Error('fail')]]),
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
const { tools: loaded } = await connectAndLoadMcpTools(mcpClient, {
|
|
331
|
+
filesystem: { command: 'npx', args: [] },
|
|
332
|
+
badserver: { command: 'nonexistent' },
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
// filesystem tools should still be loaded
|
|
336
|
+
expect(Object.keys(loaded)).toContain('mcp_filesystem_read_file');
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it('should pass sensitivity overrides through', async () => {
|
|
340
|
+
const mcpClient = createMockMcpClient([SAMPLE_TOOL]);
|
|
341
|
+
const overrides = { 'filesystem:read_file': 'safe' as ToolSensitivity };
|
|
342
|
+
|
|
343
|
+
const { tools: loaded } = await connectAndLoadMcpTools(
|
|
344
|
+
mcpClient,
|
|
345
|
+
{ filesystem: { command: 'npx' } },
|
|
346
|
+
overrides,
|
|
347
|
+
);
|
|
348
|
+
|
|
349
|
+
expect(loaded['mcp_filesystem_read_file'].sensitivity).toBe('safe');
|
|
350
|
+
});
|
|
351
|
+
});
|
|
352
|
+
});
|