@wonderwhy-er/desktop-commander 0.1.33 → 0.1.35
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/README.md +105 -36
- package/dist/command-manager.d.ts +1 -7
- package/dist/command-manager.js +31 -50
- package/dist/config-manager.d.ts +27 -16
- package/dist/config-manager.js +109 -191
- package/dist/config.js +8 -4
- package/dist/error-handlers.d.ts +7 -0
- package/dist/error-handlers.js +15 -0
- package/dist/handlers/command-handlers.d.ts +4 -18
- package/dist/handlers/command-handlers.js +15 -3
- package/dist/handlers/edit-search-handlers.d.ts +3 -12
- package/dist/handlers/edit-search-handlers.js +10 -7
- package/dist/handlers/filesystem-handlers.d.ts +9 -66
- package/dist/handlers/filesystem-handlers.js +106 -65
- package/dist/handlers/index.d.ts +0 -1
- package/dist/handlers/index.js +0 -1
- package/dist/handlers/process-handlers.d.ts +3 -12
- package/dist/handlers/terminal-handlers.d.ts +5 -24
- package/dist/index.js +18 -3
- package/dist/sandbox/index.d.ts +9 -0
- package/dist/sandbox/index.js +50 -0
- package/dist/sandbox/mac-sandbox.d.ts +19 -0
- package/dist/sandbox/mac-sandbox.js +174 -0
- package/dist/server.js +156 -176
- package/dist/setup-claude-server.js +98 -49
- package/dist/terminal-manager.d.ts +1 -1
- package/dist/terminal-manager.js +26 -4
- package/dist/tools/config.d.ts +0 -58
- package/dist/tools/config.js +44 -107
- package/dist/tools/debug-path.d.ts +1 -0
- package/dist/tools/debug-path.js +44 -0
- package/dist/tools/edit.d.ts +3 -1
- package/dist/tools/edit.js +19 -7
- package/dist/tools/execute.d.ts +4 -18
- package/dist/tools/execute.js +27 -8
- package/dist/tools/filesystem-fixed.d.ts +22 -0
- package/dist/tools/filesystem-fixed.js +176 -0
- package/dist/tools/filesystem.d.ts +4 -6
- package/dist/tools/filesystem.js +106 -75
- package/dist/tools/process.d.ts +3 -12
- package/dist/tools/process.js +12 -3
- package/dist/tools/schemas.d.ts +15 -14
- package/dist/tools/schemas.js +10 -6
- package/dist/tools/search.js +3 -3
- package/dist/types.d.ts +12 -0
- package/dist/utils.d.ts +5 -0
- package/dist/utils.js +92 -32
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -2
|
@@ -1,6 +1,19 @@
|
|
|
1
|
-
import { readFile, readMultipleFiles, writeFile, createDirectory, listDirectory, moveFile, searchFiles, getFileInfo
|
|
1
|
+
import { readFile, readMultipleFiles, writeFile, createDirectory, listDirectory, moveFile, searchFiles, getFileInfo } from '../tools/filesystem.js';
|
|
2
2
|
import { withTimeout } from '../utils.js';
|
|
3
|
+
import { createErrorResponse } from '../error-handlers.js';
|
|
3
4
|
import { ReadFileArgsSchema, ReadMultipleFilesArgsSchema, WriteFileArgsSchema, CreateDirectoryArgsSchema, ListDirectoryArgsSchema, MoveFileArgsSchema, SearchFilesArgsSchema, GetFileInfoArgsSchema } from '../tools/schemas.js';
|
|
5
|
+
/**
|
|
6
|
+
* Helper function to check if path contains an error
|
|
7
|
+
*/
|
|
8
|
+
function isErrorPath(path) {
|
|
9
|
+
return path.startsWith('__ERROR__:');
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Extract error message from error path
|
|
13
|
+
*/
|
|
14
|
+
function getErrorFromPath(path) {
|
|
15
|
+
return path.substring('__ERROR__:'.length).trim();
|
|
16
|
+
}
|
|
4
17
|
/**
|
|
5
18
|
* Handle read_file command
|
|
6
19
|
*/
|
|
@@ -8,8 +21,7 @@ export async function handleReadFile(args) {
|
|
|
8
21
|
const HANDLER_TIMEOUT = 60000; // 60 seconds total operation timeout
|
|
9
22
|
const readFileOperation = async () => {
|
|
10
23
|
const parsed = ReadFileArgsSchema.parse(args);
|
|
11
|
-
|
|
12
|
-
const fileResult = await readFile(parsed.path, true, parsed.isUrl);
|
|
24
|
+
const fileResult = await readFile(parsed.path, parsed.isUrl);
|
|
13
25
|
if (fileResult.isImage) {
|
|
14
26
|
// For image files, return as an image content type
|
|
15
27
|
return {
|
|
@@ -34,9 +46,12 @@ export async function handleReadFile(args) {
|
|
|
34
46
|
}
|
|
35
47
|
};
|
|
36
48
|
// Execute with timeout at the handler level
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
49
|
+
const result = await withTimeout(readFileOperation(), HANDLER_TIMEOUT, 'Read file handler operation', null);
|
|
50
|
+
if (result == null) {
|
|
51
|
+
// Handles the impossible case where withTimeout resolves to null instead of throwing
|
|
52
|
+
throw new Error('Failed to read the file');
|
|
53
|
+
}
|
|
54
|
+
return result;
|
|
40
55
|
}
|
|
41
56
|
/**
|
|
42
57
|
* Handle read_multiple_files command
|
|
@@ -86,94 +101,120 @@ export async function handleReadMultipleFiles(args) {
|
|
|
86
101
|
* Handle write_file command
|
|
87
102
|
*/
|
|
88
103
|
export async function handleWriteFile(args) {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
104
|
+
try {
|
|
105
|
+
const parsed = WriteFileArgsSchema.parse(args);
|
|
106
|
+
await writeFile(parsed.path, parsed.content);
|
|
107
|
+
return {
|
|
108
|
+
content: [{ type: "text", text: `Successfully wrote to ${parsed.path}` }],
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
113
|
+
return createErrorResponse(errorMessage);
|
|
114
|
+
}
|
|
94
115
|
}
|
|
95
116
|
/**
|
|
96
117
|
* Handle create_directory command
|
|
97
118
|
*/
|
|
98
119
|
export async function handleCreateDirectory(args) {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
120
|
+
try {
|
|
121
|
+
const parsed = CreateDirectoryArgsSchema.parse(args);
|
|
122
|
+
await createDirectory(parsed.path);
|
|
123
|
+
return {
|
|
124
|
+
content: [{ type: "text", text: `Successfully created directory ${parsed.path}` }],
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
129
|
+
return createErrorResponse(errorMessage);
|
|
130
|
+
}
|
|
104
131
|
}
|
|
105
132
|
/**
|
|
106
133
|
* Handle list_directory command
|
|
107
134
|
*/
|
|
108
135
|
export async function handleListDirectory(args) {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
136
|
+
try {
|
|
137
|
+
const parsed = ListDirectoryArgsSchema.parse(args);
|
|
138
|
+
const entries = await listDirectory(parsed.path);
|
|
139
|
+
return {
|
|
140
|
+
content: [{ type: "text", text: entries.join('\n') }],
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
145
|
+
return createErrorResponse(errorMessage);
|
|
146
|
+
}
|
|
114
147
|
}
|
|
115
148
|
/**
|
|
116
149
|
* Handle move_file command
|
|
117
150
|
*/
|
|
118
151
|
export async function handleMoveFile(args) {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
152
|
+
try {
|
|
153
|
+
const parsed = MoveFileArgsSchema.parse(args);
|
|
154
|
+
await moveFile(parsed.source, parsed.destination);
|
|
155
|
+
return {
|
|
156
|
+
content: [{ type: "text", text: `Successfully moved ${parsed.source} to ${parsed.destination}` }],
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
161
|
+
return createErrorResponse(errorMessage);
|
|
162
|
+
}
|
|
124
163
|
}
|
|
125
164
|
/**
|
|
126
165
|
* Handle search_files command
|
|
127
166
|
*/
|
|
128
167
|
export async function handleSearchFiles(args) {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
168
|
+
try {
|
|
169
|
+
const parsed = SearchFilesArgsSchema.parse(args);
|
|
170
|
+
const timeoutMs = parsed.timeoutMs || 30000; // 30 seconds default
|
|
171
|
+
// Apply timeout at the handler level
|
|
172
|
+
const searchOperation = async () => {
|
|
173
|
+
return await searchFiles(parsed.path, parsed.pattern);
|
|
174
|
+
};
|
|
175
|
+
// Use withTimeout at the handler level
|
|
176
|
+
const results = await withTimeout(searchOperation(), timeoutMs, 'File search operation', [] // Empty array as default on timeout
|
|
177
|
+
);
|
|
178
|
+
if (results.length === 0) {
|
|
179
|
+
// Similar approach as in handleSearchCode
|
|
180
|
+
if (timeoutMs > 0) {
|
|
181
|
+
return {
|
|
182
|
+
content: [{ type: "text", text: `No matches found or search timed out after ${timeoutMs}ms.` }],
|
|
183
|
+
};
|
|
184
|
+
}
|
|
141
185
|
return {
|
|
142
|
-
content: [{ type: "text", text:
|
|
186
|
+
content: [{ type: "text", text: "No matches found" }],
|
|
143
187
|
};
|
|
144
188
|
}
|
|
145
189
|
return {
|
|
146
|
-
content: [{ type: "text", text:
|
|
190
|
+
content: [{ type: "text", text: results.join('\n') }],
|
|
147
191
|
};
|
|
148
192
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
193
|
+
catch (error) {
|
|
194
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
195
|
+
return createErrorResponse(errorMessage);
|
|
196
|
+
}
|
|
152
197
|
}
|
|
153
198
|
/**
|
|
154
199
|
* Handle get_file_info command
|
|
155
200
|
*/
|
|
156
201
|
export async function handleGetFileInfo(args) {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
.
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
return {
|
|
174
|
-
content: [{
|
|
175
|
-
type: "text",
|
|
176
|
-
text: `Allowed directories:\n${directories.join('\n')}`
|
|
177
|
-
}],
|
|
178
|
-
};
|
|
202
|
+
try {
|
|
203
|
+
const parsed = GetFileInfoArgsSchema.parse(args);
|
|
204
|
+
const info = await getFileInfo(parsed.path);
|
|
205
|
+
return {
|
|
206
|
+
content: [{
|
|
207
|
+
type: "text",
|
|
208
|
+
text: Object.entries(info)
|
|
209
|
+
.map(([key, value]) => `${key}: ${value}`)
|
|
210
|
+
.join('\n')
|
|
211
|
+
}],
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
catch (error) {
|
|
215
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
216
|
+
return createErrorResponse(errorMessage);
|
|
217
|
+
}
|
|
179
218
|
}
|
|
219
|
+
// The listAllowedDirectories function has been removed
|
|
220
|
+
// Use get_config to retrieve the allowedDirectories configuration
|
package/dist/handlers/index.d.ts
CHANGED
package/dist/handlers/index.js
CHANGED
|
@@ -1,18 +1,9 @@
|
|
|
1
|
+
import { ServerResult } from '../types.js';
|
|
1
2
|
/**
|
|
2
3
|
* Handle list_processes command
|
|
3
4
|
*/
|
|
4
|
-
export declare function handleListProcesses(): Promise<
|
|
5
|
-
content: Array<{
|
|
6
|
-
type: string;
|
|
7
|
-
text: string;
|
|
8
|
-
}>;
|
|
9
|
-
}>;
|
|
5
|
+
export declare function handleListProcesses(): Promise<ServerResult>;
|
|
10
6
|
/**
|
|
11
7
|
* Handle kill_process command
|
|
12
8
|
*/
|
|
13
|
-
export declare function handleKillProcess(args: unknown): Promise<
|
|
14
|
-
content: {
|
|
15
|
-
type: string;
|
|
16
|
-
text: string;
|
|
17
|
-
}[];
|
|
18
|
-
}>;
|
|
9
|
+
export declare function handleKillProcess(args: unknown): Promise<ServerResult>;
|
|
@@ -1,36 +1,17 @@
|
|
|
1
|
+
import { ServerResult } from '../types.js';
|
|
1
2
|
/**
|
|
2
3
|
* Handle execute_command command
|
|
3
4
|
*/
|
|
4
|
-
export declare function handleExecuteCommand(args: unknown): Promise<
|
|
5
|
-
content: {
|
|
6
|
-
type: string;
|
|
7
|
-
text: string;
|
|
8
|
-
}[];
|
|
9
|
-
}>;
|
|
5
|
+
export declare function handleExecuteCommand(args: unknown): Promise<ServerResult>;
|
|
10
6
|
/**
|
|
11
7
|
* Handle read_output command
|
|
12
8
|
*/
|
|
13
|
-
export declare function handleReadOutput(args: unknown): Promise<
|
|
14
|
-
content: {
|
|
15
|
-
type: string;
|
|
16
|
-
text: string;
|
|
17
|
-
}[];
|
|
18
|
-
}>;
|
|
9
|
+
export declare function handleReadOutput(args: unknown): Promise<ServerResult>;
|
|
19
10
|
/**
|
|
20
11
|
* Handle force_terminate command
|
|
21
12
|
*/
|
|
22
|
-
export declare function handleForceTerminate(args: unknown): Promise<
|
|
23
|
-
content: {
|
|
24
|
-
type: string;
|
|
25
|
-
text: string;
|
|
26
|
-
}[];
|
|
27
|
-
}>;
|
|
13
|
+
export declare function handleForceTerminate(args: unknown): Promise<ServerResult>;
|
|
28
14
|
/**
|
|
29
15
|
* Handle list_sessions command
|
|
30
16
|
*/
|
|
31
|
-
export declare function handleListSessions(): Promise<
|
|
32
|
-
content: {
|
|
33
|
-
type: string;
|
|
34
|
-
text: string;
|
|
35
|
-
}[];
|
|
36
|
-
}>;
|
|
17
|
+
export declare function handleListSessions(): Promise<ServerResult>;
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { FilteredStdioServerTransport } from './custom-stdio.js';
|
|
3
3
|
import { server } from './server.js';
|
|
4
|
-
import {
|
|
4
|
+
import { configManager } from './config-manager.js';
|
|
5
5
|
import { join, dirname } from 'path';
|
|
6
6
|
import { fileURLToPath, pathToFileURL } from 'url';
|
|
7
7
|
import { platform } from 'os';
|
|
@@ -81,12 +81,25 @@ async function runServer() {
|
|
|
81
81
|
process.exit(1);
|
|
82
82
|
});
|
|
83
83
|
capture('run_server_start');
|
|
84
|
-
|
|
85
|
-
|
|
84
|
+
try {
|
|
85
|
+
console.error("Loading configuration...");
|
|
86
|
+
await configManager.loadConfig();
|
|
87
|
+
console.error("Configuration loaded successfully");
|
|
88
|
+
}
|
|
89
|
+
catch (configError) {
|
|
90
|
+
console.error(`Failed to load configuration: ${configError instanceof Error ? configError.message : String(configError)}`);
|
|
91
|
+
console.error(configError instanceof Error && configError.stack ? configError.stack : 'No stack trace available');
|
|
92
|
+
console.error("Continuing with in-memory configuration only");
|
|
93
|
+
// Continue anyway - we'll use an in-memory config
|
|
94
|
+
}
|
|
95
|
+
console.error("Connecting server...");
|
|
86
96
|
await server.connect(transport);
|
|
97
|
+
console.error("Server connected successfully");
|
|
87
98
|
}
|
|
88
99
|
catch (error) {
|
|
89
100
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
101
|
+
console.error(`FATAL ERROR: ${errorMessage}`);
|
|
102
|
+
console.error(error instanceof Error && error.stack ? error.stack : 'No stack trace available');
|
|
90
103
|
process.stderr.write(JSON.stringify({
|
|
91
104
|
type: 'error',
|
|
92
105
|
timestamp: new Date().toISOString(),
|
|
@@ -100,6 +113,8 @@ async function runServer() {
|
|
|
100
113
|
}
|
|
101
114
|
runServer().catch(async (error) => {
|
|
102
115
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
116
|
+
console.error(`RUNTIME ERROR: ${errorMessage}`);
|
|
117
|
+
console.error(error instanceof Error && error.stack ? error.stack : 'No stack trace available');
|
|
103
118
|
process.stderr.write(JSON.stringify({
|
|
104
119
|
type: 'error',
|
|
105
120
|
timestamp: new Date().toISOString(),
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { CommandExecutionResult } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Platform detection and sandbox execution router
|
|
4
|
+
*/
|
|
5
|
+
export declare function executeSandboxedCommand(command: string, timeoutMs?: number, shell?: string): Promise<CommandExecutionResult>;
|
|
6
|
+
/**
|
|
7
|
+
* Check if sandboxed execution is available for the current platform
|
|
8
|
+
*/
|
|
9
|
+
export declare function isSandboxAvailable(): boolean;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import os from 'os';
|
|
2
|
+
import { executeSandboxedCommand as macExecuteSandboxedCommand } from './mac-sandbox.js';
|
|
3
|
+
import { configManager } from '../config-manager.js';
|
|
4
|
+
/**
|
|
5
|
+
* Platform detection and sandbox execution router
|
|
6
|
+
*/
|
|
7
|
+
export async function executeSandboxedCommand(command, timeoutMs = 30000, shell) {
|
|
8
|
+
// Get the allowed directories from config
|
|
9
|
+
const config = await configManager.getConfig();
|
|
10
|
+
const allowedDirectories = config.allowedDirectories || [os.homedir()];
|
|
11
|
+
const platform = os.platform();
|
|
12
|
+
// Platform-specific sandbox execution
|
|
13
|
+
switch (platform) {
|
|
14
|
+
case 'darwin': // macOS
|
|
15
|
+
try {
|
|
16
|
+
const result = await macExecuteSandboxedCommand(command, allowedDirectories, timeoutMs);
|
|
17
|
+
return {
|
|
18
|
+
pid: result.pid,
|
|
19
|
+
output: result.output,
|
|
20
|
+
isBlocked: result.isBlocked
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
console.error('Mac sandbox execution error:', error);
|
|
25
|
+
return {
|
|
26
|
+
pid: -1,
|
|
27
|
+
output: `Sandbox execution error: ${error instanceof Error ? error.message : String(error)}`,
|
|
28
|
+
isBlocked: false
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
// Add cases for other platforms when implemented
|
|
32
|
+
// case 'linux':
|
|
33
|
+
// case 'win32':
|
|
34
|
+
default:
|
|
35
|
+
// For unsupported platforms, return an error
|
|
36
|
+
return {
|
|
37
|
+
pid: -1,
|
|
38
|
+
output: `Sandbox execution not supported on ${platform}. Command was not executed: ${command}`,
|
|
39
|
+
isBlocked: false
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Check if sandboxed execution is available for the current platform
|
|
45
|
+
*/
|
|
46
|
+
export function isSandboxAvailable() {
|
|
47
|
+
const platform = os.platform();
|
|
48
|
+
// Currently only implemented for macOS
|
|
49
|
+
return platform === 'darwin';
|
|
50
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generate a temporary sandbox profile for macOS that restricts access to allowed directories
|
|
3
|
+
* @param allowedDirectories Array of directories that should be accessible
|
|
4
|
+
* @returns Path to the generated sandbox profile file
|
|
5
|
+
*/
|
|
6
|
+
export declare function generateSandboxProfile(allowedDirectories: string[]): Promise<string>;
|
|
7
|
+
/**
|
|
8
|
+
* Execute a command in a macOS sandbox with access restricted to allowed directories
|
|
9
|
+
* @param command Command to execute
|
|
10
|
+
* @param allowedDirectories Array of allowed directory paths
|
|
11
|
+
* @param options Additional execution options
|
|
12
|
+
* @returns Promise resolving to the execution result
|
|
13
|
+
*/
|
|
14
|
+
export declare function executeSandboxedCommand(command: string, allowedDirectories: string[], timeoutMs?: number): Promise<{
|
|
15
|
+
output: string;
|
|
16
|
+
exitCode: number | null;
|
|
17
|
+
isBlocked: boolean;
|
|
18
|
+
pid: number;
|
|
19
|
+
}>;
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import fs from 'fs/promises';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
/**
|
|
6
|
+
* Generate a temporary sandbox profile for macOS that restricts access to allowed directories
|
|
7
|
+
* @param allowedDirectories Array of directories that should be accessible
|
|
8
|
+
* @returns Path to the generated sandbox profile file
|
|
9
|
+
*/
|
|
10
|
+
export async function generateSandboxProfile(allowedDirectories) {
|
|
11
|
+
// Create a temporary directory for the sandbox profile
|
|
12
|
+
const tempDir = path.join(os.tmpdir(), 'claude-server-sandbox');
|
|
13
|
+
// Ensure temp directory exists
|
|
14
|
+
try {
|
|
15
|
+
// Check if directory exists first
|
|
16
|
+
try {
|
|
17
|
+
await fs.access(tempDir);
|
|
18
|
+
console.log(`Temp directory exists: ${tempDir}`);
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
// Directory doesn't exist, create it
|
|
22
|
+
console.log(`Creating temp directory: ${tempDir}`);
|
|
23
|
+
await fs.mkdir(tempDir, { recursive: true });
|
|
24
|
+
console.log(`Temp directory created: ${tempDir}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
console.error('Error creating temp directory for sandbox:', error);
|
|
29
|
+
throw new Error(`Failed to create sandbox temp directory: ${error}`);
|
|
30
|
+
}
|
|
31
|
+
// Create the sandbox profile content - based on our tests, we need a simpler approach
|
|
32
|
+
// that just allows by default and then adds specific permissions for allowed directories
|
|
33
|
+
// Use a more restrictive approach - deny all file access, then explicitly allow only what we need
|
|
34
|
+
let profileContent = `(version 1)
|
|
35
|
+
(debug deny)
|
|
36
|
+
(allow default)
|
|
37
|
+
(deny file-read* file-write*)
|
|
38
|
+
`;
|
|
39
|
+
// Add explicit permissions for allowed directories
|
|
40
|
+
for (const dir of allowedDirectories) {
|
|
41
|
+
// Ensure path is absolute
|
|
42
|
+
const absPath = path.resolve(dir);
|
|
43
|
+
profileContent += `(allow file-read* file-write* (subpath "${absPath}"))\n`;
|
|
44
|
+
}
|
|
45
|
+
// Add system paths needed for basic command execution
|
|
46
|
+
profileContent += `
|
|
47
|
+
(allow file-read* (subpath "/usr"))
|
|
48
|
+
(allow file-read* (subpath "/bin"))
|
|
49
|
+
(allow file-read* (subpath "/sbin"))
|
|
50
|
+
(allow file-read* (subpath "/Library"))
|
|
51
|
+
(allow file-read* (subpath "/System"))
|
|
52
|
+
(allow file-read* (subpath "/tmp"))
|
|
53
|
+
(allow file-read* (subpath "/dev"))
|
|
54
|
+
(allow file-read* (subpath "/etc"))
|
|
55
|
+
(allow file-read* (subpath "${os.homedir()}/.zshrc"))
|
|
56
|
+
(allow file-read* (subpath "${os.homedir()}/.bash_profile"))
|
|
57
|
+
(allow file-read* (subpath "${os.homedir()}/.bashrc"))
|
|
58
|
+
(allow process-exec)
|
|
59
|
+
(allow process-fork)
|
|
60
|
+
(allow mach-lookup)
|
|
61
|
+
(allow network-outbound)
|
|
62
|
+
(allow system-socket)
|
|
63
|
+
(allow sysctl-read)
|
|
64
|
+
`;
|
|
65
|
+
// Add comment for debugging
|
|
66
|
+
profileContent += `\n; Allowed directories: ${allowedDirectories.join(', ')}\n`;
|
|
67
|
+
// Write the profile to a temporary file
|
|
68
|
+
const profilePath = path.join(tempDir, 'sandbox-profile.sb');
|
|
69
|
+
try {
|
|
70
|
+
// Write the profile with verbose logging
|
|
71
|
+
console.log(`Writing sandbox profile to: ${profilePath}`);
|
|
72
|
+
await fs.writeFile(profilePath, profileContent, 'utf-8');
|
|
73
|
+
// Verify the file was created
|
|
74
|
+
await fs.access(profilePath);
|
|
75
|
+
console.log(`Sandbox profile created successfully`);
|
|
76
|
+
// Log the profile content for debugging
|
|
77
|
+
console.log(`Sandbox profile content:\n${profileContent}`);
|
|
78
|
+
return profilePath;
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
console.error(`Error creating sandbox profile at ${profilePath}:`, error);
|
|
82
|
+
throw new Error(`Failed to create sandbox profile: ${error}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Execute a command in a macOS sandbox with access restricted to allowed directories
|
|
87
|
+
* @param command Command to execute
|
|
88
|
+
* @param allowedDirectories Array of allowed directory paths
|
|
89
|
+
* @param options Additional execution options
|
|
90
|
+
* @returns Promise resolving to the execution result
|
|
91
|
+
*/
|
|
92
|
+
export async function executeSandboxedCommand(command, allowedDirectories, timeoutMs = 30000) {
|
|
93
|
+
try {
|
|
94
|
+
// Generate the sandbox profile
|
|
95
|
+
const profilePath = await generateSandboxProfile(allowedDirectories);
|
|
96
|
+
// Create a wrapper script that will perform additional path checks
|
|
97
|
+
// This is a belt-and-suspenders approach since our tests show the sandbox
|
|
98
|
+
// doesn't perfectly restrict access to only allowed directories
|
|
99
|
+
const wrapperScriptPath = path.join(os.tmpdir(), `claude-sandbox-wrapper-${Date.now()}.sh`);
|
|
100
|
+
// Generate a script that does additional path validation
|
|
101
|
+
// This will extract file paths from the command and verify they're in allowed directories
|
|
102
|
+
const wrapperScript = `#!/bin/sh
|
|
103
|
+
# Wrapper script for sandboxed execution
|
|
104
|
+
# Only allows access to specific directories: ${allowedDirectories.join(', ')}
|
|
105
|
+
|
|
106
|
+
# The actual command to run
|
|
107
|
+
COMMAND="${command.replace(/"/g, '\\"')}"
|
|
108
|
+
|
|
109
|
+
# Run the command in sandbox
|
|
110
|
+
sandbox-exec -f "${profilePath}" /bin/sh -c "$COMMAND"
|
|
111
|
+
EXIT_CODE=$?
|
|
112
|
+
|
|
113
|
+
# Return the exit code from the sandbox
|
|
114
|
+
exit $EXIT_CODE
|
|
115
|
+
`;
|
|
116
|
+
// Write the wrapper script
|
|
117
|
+
await fs.writeFile(wrapperScriptPath, wrapperScript, { mode: 0o755 });
|
|
118
|
+
// Log what we're doing
|
|
119
|
+
console.log(`Executing sandboxed command via wrapper script`);
|
|
120
|
+
console.log(`Command: ${command}`);
|
|
121
|
+
console.log(`Allowed directories: ${allowedDirectories.join(', ')}`);
|
|
122
|
+
// Execute the wrapper script
|
|
123
|
+
const process = spawn(wrapperScriptPath, []);
|
|
124
|
+
let output = '';
|
|
125
|
+
let isBlocked = false;
|
|
126
|
+
// Set up timeout
|
|
127
|
+
const timeoutId = setTimeout(() => {
|
|
128
|
+
isBlocked = true;
|
|
129
|
+
}, timeoutMs);
|
|
130
|
+
// Handle output
|
|
131
|
+
process.stdout.on('data', (data) => {
|
|
132
|
+
const text = data.toString();
|
|
133
|
+
console.log(`Sandbox stdout: ${text}`);
|
|
134
|
+
output += text;
|
|
135
|
+
});
|
|
136
|
+
process.stderr.on('data', (data) => {
|
|
137
|
+
const text = data.toString();
|
|
138
|
+
console.log(`Sandbox stderr: ${text}`);
|
|
139
|
+
output += text;
|
|
140
|
+
});
|
|
141
|
+
// Return a promise that resolves when the process exits
|
|
142
|
+
return new Promise((resolve) => {
|
|
143
|
+
process.on('exit', (code) => {
|
|
144
|
+
clearTimeout(timeoutId);
|
|
145
|
+
console.log(`Sandbox process exited with code: ${code}`);
|
|
146
|
+
// Clean up the temporary files
|
|
147
|
+
Promise.all([
|
|
148
|
+
fs.unlink(profilePath).catch(err => {
|
|
149
|
+
console.error('Error removing temporary sandbox profile:', err);
|
|
150
|
+
}),
|
|
151
|
+
fs.unlink(wrapperScriptPath).catch(err => {
|
|
152
|
+
console.error('Error removing temporary wrapper script:', err);
|
|
153
|
+
})
|
|
154
|
+
]).finally(() => {
|
|
155
|
+
resolve({
|
|
156
|
+
output,
|
|
157
|
+
exitCode: code,
|
|
158
|
+
isBlocked,
|
|
159
|
+
pid: process.pid || -1
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
console.error('Error in sandbox execution:', error);
|
|
167
|
+
return {
|
|
168
|
+
output: `Sandbox execution error: ${error instanceof Error ? error.message : String(error)}`,
|
|
169
|
+
exitCode: 1,
|
|
170
|
+
isBlocked: false,
|
|
171
|
+
pid: -1
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
}
|