gclm-code 1.0.0 → 1.0.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/README.md +1 -1
- package/bin/gc.js +53 -25
- package/bin/install-runtime.js +253 -0
- package/package.json +10 -5
- package/vendor/manifest.json +92 -0
- package/vendor/modules/node_modules/@ant/claude-for-chrome-mcp/package.json +9 -0
- package/vendor/modules/node_modules/@ant/claude-for-chrome-mcp/src/bridgeClient.ts +1126 -0
- package/vendor/modules/node_modules/@ant/claude-for-chrome-mcp/src/browserTools.ts +546 -0
- package/vendor/modules/node_modules/@ant/claude-for-chrome-mcp/src/index.ts +15 -0
- package/vendor/modules/node_modules/@ant/claude-for-chrome-mcp/src/mcpServer.ts +96 -0
- package/vendor/modules/node_modules/@ant/claude-for-chrome-mcp/src/mcpSocketClient.ts +493 -0
- package/vendor/modules/node_modules/@ant/claude-for-chrome-mcp/src/mcpSocketPool.ts +327 -0
- package/vendor/modules/node_modules/@ant/claude-for-chrome-mcp/src/toolCalls.ts +301 -0
- package/vendor/modules/node_modules/@ant/claude-for-chrome-mcp/src/types.ts +134 -0
- package/vendor/modules/node_modules/@ant/computer-use-input/package.json +9 -0
- package/vendor/modules/node_modules/@ant/computer-use-input/src/driver-jxa.js +341 -0
- package/vendor/modules/node_modules/@ant/computer-use-input/src/driver-swift.swift +417 -0
- package/vendor/modules/node_modules/@ant/computer-use-input/src/implementation.js +204 -0
- package/vendor/modules/node_modules/@ant/computer-use-input/src/index.js +5 -0
- package/vendor/modules/node_modules/@ant/computer-use-mcp/package.json +11 -0
- package/vendor/modules/node_modules/@ant/computer-use-mcp/src/deniedApps.ts +553 -0
- package/vendor/modules/node_modules/@ant/computer-use-mcp/src/imageResize.ts +108 -0
- package/vendor/modules/node_modules/@ant/computer-use-mcp/src/index.ts +69 -0
- package/vendor/modules/node_modules/@ant/computer-use-mcp/src/keyBlocklist.ts +153 -0
- package/vendor/modules/node_modules/@ant/computer-use-mcp/src/mcpServer.ts +313 -0
- package/vendor/modules/node_modules/@ant/computer-use-mcp/src/pixelCompare.ts +171 -0
- package/vendor/modules/node_modules/@ant/computer-use-mcp/src/sentinelApps.ts +43 -0
- package/vendor/modules/node_modules/@ant/computer-use-mcp/src/subGates.ts +19 -0
- package/vendor/modules/node_modules/@ant/computer-use-mcp/src/toolCalls.ts +3872 -0
- package/vendor/modules/node_modules/@ant/computer-use-mcp/src/tools.ts +706 -0
- package/vendor/modules/node_modules/@ant/computer-use-mcp/src/types.ts +635 -0
- package/vendor/modules/node_modules/@ant/computer-use-swift/package.json +9 -0
- package/vendor/modules/node_modules/@ant/computer-use-swift/src/driver-jxa.js +108 -0
- package/vendor/modules/node_modules/@ant/computer-use-swift/src/implementation.js +706 -0
- package/vendor/modules/node_modules/@ant/computer-use-swift/src/index.js +7 -0
- package/vendor/modules/node_modules/audio-capture-napi/package.json +8 -0
- package/vendor/modules/node_modules/audio-capture-napi/src/index.ts +226 -0
- package/vendor/modules/node_modules/image-processor-napi/package.json +11 -0
- package/vendor/modules/node_modules/image-processor-napi/src/index.ts +396 -0
- package/vendor/modules/node_modules/modifiers-napi/package.json +8 -0
- package/vendor/modules/node_modules/modifiers-napi/src/index.ts +79 -0
- package/vendor/modules/node_modules/url-handler-napi/package.json +8 -0
- package/vendor/modules/node_modules/url-handler-napi/src/index.ts +62 -0
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
import { promises as fsPromises } from "fs";
|
|
2
|
+
import { createConnection } from "net";
|
|
3
|
+
import type { Socket } from "net";
|
|
4
|
+
import { platform } from "os";
|
|
5
|
+
import { dirname } from "path";
|
|
6
|
+
|
|
7
|
+
import type {
|
|
8
|
+
ClaudeForChromeContext,
|
|
9
|
+
PermissionMode,
|
|
10
|
+
PermissionOverrides,
|
|
11
|
+
} from "./types.js";
|
|
12
|
+
|
|
13
|
+
export class SocketConnectionError extends Error {
|
|
14
|
+
constructor(message: string) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.name = "SocketConnectionError";
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface ToolRequest {
|
|
21
|
+
method: string; // "execute_tool"
|
|
22
|
+
params?: {
|
|
23
|
+
client_id?: string; // "desktop" | "claude-code"
|
|
24
|
+
tool?: string;
|
|
25
|
+
args?: Record<string, unknown>;
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface ToolResponse {
|
|
30
|
+
result?: unknown;
|
|
31
|
+
error?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface Notification {
|
|
35
|
+
method: string;
|
|
36
|
+
params?: Record<string, unknown>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
type SocketMessage = ToolResponse | Notification;
|
|
40
|
+
|
|
41
|
+
function isToolResponse(message: SocketMessage): message is ToolResponse {
|
|
42
|
+
return "result" in message || "error" in message;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function isNotification(message: SocketMessage): message is Notification {
|
|
46
|
+
return "method" in message && typeof message.method === "string";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
class McpSocketClient {
|
|
50
|
+
private socket: Socket | null = null;
|
|
51
|
+
private connected = false;
|
|
52
|
+
private connecting = false;
|
|
53
|
+
private responseCallback: ((response: ToolResponse) => void) | null = null;
|
|
54
|
+
private notificationHandler: ((notification: Notification) => void) | null =
|
|
55
|
+
null;
|
|
56
|
+
private responseBuffer = Buffer.alloc(0);
|
|
57
|
+
private reconnectAttempts = 0;
|
|
58
|
+
private maxReconnectAttempts = 10;
|
|
59
|
+
private reconnectDelay = 1000;
|
|
60
|
+
private reconnectTimer: NodeJS.Timeout | null = null;
|
|
61
|
+
private context: ClaudeForChromeContext;
|
|
62
|
+
// When true, disables automatic reconnection. Used by McpSocketPool which
|
|
63
|
+
// manages reconnection externally by rescanning available sockets.
|
|
64
|
+
public disableAutoReconnect = false;
|
|
65
|
+
|
|
66
|
+
constructor(context: ClaudeForChromeContext) {
|
|
67
|
+
this.context = context;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private async connect(): Promise<void> {
|
|
71
|
+
const { serverName, logger } = this.context;
|
|
72
|
+
|
|
73
|
+
if (this.connecting) {
|
|
74
|
+
logger.info(
|
|
75
|
+
`[${serverName}] Already connecting, skipping duplicate attempt`,
|
|
76
|
+
);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
this.closeSocket();
|
|
81
|
+
this.connecting = true;
|
|
82
|
+
|
|
83
|
+
const socketPath =
|
|
84
|
+
this.context.getSocketPath?.() ?? this.context.socketPath;
|
|
85
|
+
logger.info(`[${serverName}] Attempting to connect to: ${socketPath}`);
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
await this.validateSocketSecurity(socketPath);
|
|
89
|
+
} catch (error) {
|
|
90
|
+
this.connecting = false;
|
|
91
|
+
logger.info(`[${serverName}] Security validation failed:`, error);
|
|
92
|
+
// Don't retry on security failures (wrong perms/owner) - those won't
|
|
93
|
+
// self-resolve. Only the error handler retries on transient errors.
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
this.socket = createConnection(socketPath);
|
|
98
|
+
|
|
99
|
+
// Timeout the initial connection attempt - if socket file exists but native
|
|
100
|
+
// host is dead, the connect can hang indefinitely
|
|
101
|
+
const connectTimeout = setTimeout(() => {
|
|
102
|
+
if (!this.connected) {
|
|
103
|
+
logger.info(
|
|
104
|
+
`[${serverName}] Connection attempt timed out after 5000ms`,
|
|
105
|
+
);
|
|
106
|
+
this.closeSocket();
|
|
107
|
+
this.scheduleReconnect();
|
|
108
|
+
}
|
|
109
|
+
}, 5000);
|
|
110
|
+
|
|
111
|
+
this.socket.on("connect", () => {
|
|
112
|
+
clearTimeout(connectTimeout);
|
|
113
|
+
this.connected = true;
|
|
114
|
+
this.connecting = false;
|
|
115
|
+
this.reconnectAttempts = 0;
|
|
116
|
+
logger.info(`[${serverName}] Successfully connected to bridge server`);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
this.socket.on("data", (data: Buffer) => {
|
|
120
|
+
this.responseBuffer = Buffer.concat([this.responseBuffer, data]);
|
|
121
|
+
|
|
122
|
+
while (this.responseBuffer.length >= 4) {
|
|
123
|
+
const length = this.responseBuffer.readUInt32LE(0);
|
|
124
|
+
|
|
125
|
+
if (this.responseBuffer.length < 4 + length) {
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const messageBytes = this.responseBuffer.slice(4, 4 + length);
|
|
130
|
+
this.responseBuffer = this.responseBuffer.slice(4 + length);
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
const message = JSON.parse(
|
|
134
|
+
messageBytes.toString("utf-8"),
|
|
135
|
+
) as SocketMessage;
|
|
136
|
+
|
|
137
|
+
if (isNotification(message)) {
|
|
138
|
+
logger.info(
|
|
139
|
+
`[${serverName}] Received notification: ${message.method}`,
|
|
140
|
+
);
|
|
141
|
+
if (this.notificationHandler) {
|
|
142
|
+
this.notificationHandler(message);
|
|
143
|
+
}
|
|
144
|
+
} else if (isToolResponse(message)) {
|
|
145
|
+
logger.info(`[${serverName}] Received tool response: ${message}`);
|
|
146
|
+
this.handleResponse(message);
|
|
147
|
+
} else {
|
|
148
|
+
logger.info(`[${serverName}] Received unknown message: ${message}`);
|
|
149
|
+
}
|
|
150
|
+
} catch (error) {
|
|
151
|
+
logger.info(`[${serverName}] Failed to parse message:`, error);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
this.socket.on("error", (error: Error & { code?: string }) => {
|
|
157
|
+
clearTimeout(connectTimeout);
|
|
158
|
+
logger.info(`[${serverName}] Socket error (code: ${error.code}):`, error);
|
|
159
|
+
this.connected = false;
|
|
160
|
+
this.connecting = false;
|
|
161
|
+
|
|
162
|
+
if (
|
|
163
|
+
error.code &&
|
|
164
|
+
[
|
|
165
|
+
"ECONNREFUSED", // Native host not listening (stale socket)
|
|
166
|
+
"ECONNRESET", // Connection reset by peer
|
|
167
|
+
"EPIPE", // Broken pipe (native host died mid-write)
|
|
168
|
+
"ENOENT", // Socket file was deleted
|
|
169
|
+
"EOPNOTSUPP", // Socket file exists but is not a valid socket
|
|
170
|
+
"ECONNABORTED", // Connection aborted
|
|
171
|
+
].includes(error.code)
|
|
172
|
+
) {
|
|
173
|
+
this.scheduleReconnect();
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
this.socket.on("close", () => {
|
|
178
|
+
clearTimeout(connectTimeout);
|
|
179
|
+
this.connected = false;
|
|
180
|
+
this.connecting = false;
|
|
181
|
+
this.scheduleReconnect();
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private scheduleReconnect(): void {
|
|
186
|
+
const { serverName, logger } = this.context;
|
|
187
|
+
|
|
188
|
+
if (this.disableAutoReconnect) {
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (this.reconnectTimer) {
|
|
193
|
+
logger.info(`[${serverName}] Reconnect already scheduled, skipping`);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
this.reconnectAttempts++;
|
|
198
|
+
|
|
199
|
+
// Give up after extended polling (~50 min). A new ensureConnected() call
|
|
200
|
+
// from a tool request will restart the cycle if needed.
|
|
201
|
+
const maxTotalAttempts = 100;
|
|
202
|
+
if (this.reconnectAttempts > maxTotalAttempts) {
|
|
203
|
+
logger.info(
|
|
204
|
+
`[${serverName}] Giving up after ${maxTotalAttempts} attempts. Will retry on next tool call.`,
|
|
205
|
+
);
|
|
206
|
+
this.reconnectAttempts = 0;
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Use aggressive backoff for first 10 attempts, then slow poll every 30s.
|
|
211
|
+
const delay = Math.min(
|
|
212
|
+
this.reconnectDelay * Math.pow(1.5, this.reconnectAttempts - 1),
|
|
213
|
+
30000,
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
if (this.reconnectAttempts <= this.maxReconnectAttempts) {
|
|
217
|
+
logger.info(
|
|
218
|
+
`[${serverName}] Reconnecting in ${Math.round(delay)}ms (attempt ${
|
|
219
|
+
this.reconnectAttempts
|
|
220
|
+
})`,
|
|
221
|
+
);
|
|
222
|
+
} else if (this.reconnectAttempts % 10 === 0) {
|
|
223
|
+
// Log every 10th slow-poll attempt to avoid log spam
|
|
224
|
+
logger.info(
|
|
225
|
+
`[${serverName}] Still polling for native host (attempt ${this.reconnectAttempts})`,
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
this.reconnectTimer = setTimeout(() => {
|
|
230
|
+
this.reconnectTimer = null;
|
|
231
|
+
void this.connect();
|
|
232
|
+
}, delay);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
private handleResponse(response: ToolResponse): void {
|
|
236
|
+
if (this.responseCallback) {
|
|
237
|
+
const callback = this.responseCallback;
|
|
238
|
+
this.responseCallback = null;
|
|
239
|
+
callback(response);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
public setNotificationHandler(
|
|
244
|
+
handler: (notification: Notification) => void,
|
|
245
|
+
): void {
|
|
246
|
+
this.notificationHandler = handler;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
public async ensureConnected(): Promise<boolean> {
|
|
250
|
+
const { serverName } = this.context;
|
|
251
|
+
|
|
252
|
+
if (this.connected && this.socket) {
|
|
253
|
+
return true;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (!this.socket && !this.connecting) {
|
|
257
|
+
await this.connect();
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Wait for connection with timeout
|
|
261
|
+
return new Promise((resolve, reject) => {
|
|
262
|
+
let checkTimeoutId: NodeJS.Timeout | null = null;
|
|
263
|
+
|
|
264
|
+
const timeout = setTimeout(() => {
|
|
265
|
+
if (checkTimeoutId) {
|
|
266
|
+
clearTimeout(checkTimeoutId);
|
|
267
|
+
}
|
|
268
|
+
reject(
|
|
269
|
+
new SocketConnectionError(
|
|
270
|
+
`[${serverName}] Connection attempt timed out after 5000ms`,
|
|
271
|
+
),
|
|
272
|
+
);
|
|
273
|
+
}, 5000);
|
|
274
|
+
|
|
275
|
+
const checkConnection = () => {
|
|
276
|
+
if (this.connected) {
|
|
277
|
+
clearTimeout(timeout);
|
|
278
|
+
resolve(true);
|
|
279
|
+
} else {
|
|
280
|
+
checkTimeoutId = setTimeout(checkConnection, 500);
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
checkConnection();
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
private async sendRequest(
|
|
288
|
+
request: ToolRequest,
|
|
289
|
+
timeoutMs = 30000,
|
|
290
|
+
): Promise<ToolResponse> {
|
|
291
|
+
const { serverName } = this.context;
|
|
292
|
+
|
|
293
|
+
if (!this.socket) {
|
|
294
|
+
throw new SocketConnectionError(
|
|
295
|
+
`[${serverName}] Cannot send request: not connected`,
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const socket = this.socket;
|
|
300
|
+
|
|
301
|
+
return new Promise((resolve, reject) => {
|
|
302
|
+
const timeout = setTimeout(() => {
|
|
303
|
+
this.responseCallback = null;
|
|
304
|
+
reject(
|
|
305
|
+
new SocketConnectionError(
|
|
306
|
+
`[${serverName}] Tool request timed out after ${timeoutMs}ms`,
|
|
307
|
+
),
|
|
308
|
+
);
|
|
309
|
+
}, timeoutMs);
|
|
310
|
+
|
|
311
|
+
this.responseCallback = (response) => {
|
|
312
|
+
clearTimeout(timeout);
|
|
313
|
+
resolve(response);
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
const requestJson = JSON.stringify(request);
|
|
317
|
+
const requestBytes = Buffer.from(requestJson, "utf-8");
|
|
318
|
+
|
|
319
|
+
const lengthPrefix = Buffer.allocUnsafe(4);
|
|
320
|
+
lengthPrefix.writeUInt32LE(requestBytes.length, 0);
|
|
321
|
+
|
|
322
|
+
const message = Buffer.concat([lengthPrefix, requestBytes]);
|
|
323
|
+
socket.write(message);
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
public async callTool(
|
|
328
|
+
name: string,
|
|
329
|
+
args: Record<string, unknown>,
|
|
330
|
+
_permissionOverrides?: PermissionOverrides,
|
|
331
|
+
): Promise<unknown> {
|
|
332
|
+
const request: ToolRequest = {
|
|
333
|
+
method: "execute_tool",
|
|
334
|
+
params: {
|
|
335
|
+
client_id: this.context.clientTypeId,
|
|
336
|
+
tool: name,
|
|
337
|
+
args,
|
|
338
|
+
},
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
return this.sendRequestWithRetry(request);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Send a request with automatic retry on connection errors.
|
|
346
|
+
*
|
|
347
|
+
* On connection error or timeout, the native host may be a zombie (connected
|
|
348
|
+
* to dead Chrome). Force reconnect to pick up a fresh native host process
|
|
349
|
+
* and retry once.
|
|
350
|
+
*/
|
|
351
|
+
private async sendRequestWithRetry(request: ToolRequest): Promise<unknown> {
|
|
352
|
+
const { serverName, logger } = this.context;
|
|
353
|
+
|
|
354
|
+
try {
|
|
355
|
+
return await this.sendRequest(request);
|
|
356
|
+
} catch (error) {
|
|
357
|
+
if (!(error instanceof SocketConnectionError)) {
|
|
358
|
+
throw error;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
logger.info(
|
|
362
|
+
`[${serverName}] Connection error, forcing reconnect and retrying: ${error.message}`,
|
|
363
|
+
);
|
|
364
|
+
|
|
365
|
+
this.closeSocket();
|
|
366
|
+
await this.ensureConnected();
|
|
367
|
+
|
|
368
|
+
return await this.sendRequest(request);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
public async setPermissionMode(
|
|
373
|
+
_mode: PermissionMode,
|
|
374
|
+
_allowedDomains?: string[],
|
|
375
|
+
): Promise<void> {
|
|
376
|
+
// No-op: permission mode is only supported over the bridge (WebSocket) transport
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
public isConnected(): boolean {
|
|
380
|
+
return this.connected;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
private closeSocket(): void {
|
|
384
|
+
if (this.socket) {
|
|
385
|
+
this.socket.removeAllListeners();
|
|
386
|
+
this.socket.end();
|
|
387
|
+
this.socket.destroy();
|
|
388
|
+
this.socket = null;
|
|
389
|
+
}
|
|
390
|
+
this.connected = false;
|
|
391
|
+
this.connecting = false;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
private cleanup(): void {
|
|
395
|
+
if (this.reconnectTimer) {
|
|
396
|
+
clearTimeout(this.reconnectTimer);
|
|
397
|
+
this.reconnectTimer = null;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
this.closeSocket();
|
|
401
|
+
this.reconnectAttempts = 0;
|
|
402
|
+
this.responseBuffer = Buffer.alloc(0);
|
|
403
|
+
this.responseCallback = null;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
public disconnect(): void {
|
|
407
|
+
this.cleanup();
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
private async validateSocketSecurity(socketPath: string): Promise<void> {
|
|
411
|
+
const { serverName, logger } = this.context;
|
|
412
|
+
if (platform() === "win32") {
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
try {
|
|
416
|
+
// Validate the parent directory permissions if it's the socket directory
|
|
417
|
+
// (not /tmp itself, which has mode 1777 for legacy single-socket paths)
|
|
418
|
+
const dirPath = dirname(socketPath);
|
|
419
|
+
const dirBasename = dirPath.split("/").pop() || "";
|
|
420
|
+
const isSocketDir = dirBasename.startsWith("claude-mcp-browser-bridge-");
|
|
421
|
+
if (isSocketDir) {
|
|
422
|
+
try {
|
|
423
|
+
const dirStats = await fsPromises.stat(dirPath);
|
|
424
|
+
if (dirStats.isDirectory()) {
|
|
425
|
+
const dirMode = dirStats.mode & 0o777;
|
|
426
|
+
if (dirMode !== 0o700) {
|
|
427
|
+
throw new Error(
|
|
428
|
+
`[${serverName}] Insecure socket directory permissions: ${dirMode.toString(
|
|
429
|
+
8,
|
|
430
|
+
)} (expected 0700). Directory may have been tampered with.`,
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
const currentUid = process.getuid?.();
|
|
434
|
+
if (currentUid !== undefined && dirStats.uid !== currentUid) {
|
|
435
|
+
throw new Error(
|
|
436
|
+
`Socket directory not owned by current user (uid: ${currentUid}, dir uid: ${dirStats.uid}). ` +
|
|
437
|
+
`Potential security risk.`,
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
} catch (dirError) {
|
|
442
|
+
if ((dirError as NodeJS.ErrnoException).code !== "ENOENT") {
|
|
443
|
+
throw dirError;
|
|
444
|
+
}
|
|
445
|
+
// Directory doesn't exist yet - native host will create it
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const stats = await fsPromises.stat(socketPath);
|
|
450
|
+
|
|
451
|
+
if (!stats.isSocket()) {
|
|
452
|
+
throw new Error(
|
|
453
|
+
`[${serverName}] Path exists but it's not a socket: ${socketPath}`,
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const mode = stats.mode & 0o777;
|
|
458
|
+
if (mode !== 0o600) {
|
|
459
|
+
throw new Error(
|
|
460
|
+
`[${serverName}] Insecure socket permissions: ${mode.toString(
|
|
461
|
+
8,
|
|
462
|
+
)} (expected 0600). Socket may have been tampered with.`,
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const currentUid = process.getuid?.();
|
|
467
|
+
if (currentUid !== undefined && stats.uid !== currentUid) {
|
|
468
|
+
throw new Error(
|
|
469
|
+
`Socket not owned by current user (uid: ${currentUid}, socket uid: ${stats.uid}). ` +
|
|
470
|
+
`Potential security risk.`,
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
logger.info(`[${serverName}] Socket security validation passed`);
|
|
475
|
+
} catch (error) {
|
|
476
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
477
|
+
logger.info(
|
|
478
|
+
`[${serverName}] Socket not found, will be created by server`,
|
|
479
|
+
);
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
throw error;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
export function createMcpSocketClient(
|
|
488
|
+
context: ClaudeForChromeContext,
|
|
489
|
+
): McpSocketClient {
|
|
490
|
+
return new McpSocketClient(context);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
export type { McpSocketClient };
|