@ai-support-agent/cli 0.1.30-beta.0 → 0.1.30-beta.2
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/browser/playwright-test-runner.d.ts +24 -0
- package/dist/browser/playwright-test-runner.d.ts.map +1 -0
- package/dist/browser/playwright-test-runner.js +191 -0
- package/dist/browser/playwright-test-runner.js.map +1 -0
- package/dist/commands/e2e-test-executor.d.ts +1 -2
- package/dist/commands/e2e-test-executor.d.ts.map +1 -1
- package/dist/commands/e2e-test-executor.js +19 -252
- package/dist/commands/e2e-test-executor.js.map +1 -1
- package/dist/mcp/tools/browser/browser-session.d.ts +119 -8
- package/dist/mcp/tools/browser/browser-session.d.ts.map +1 -1
- package/dist/mcp/tools/browser/browser-session.js +214 -1
- package/dist/mcp/tools/browser/browser-session.js.map +1 -1
- package/dist/mcp/tools/browser/element-info.d.ts +9 -0
- package/dist/mcp/tools/browser/element-info.d.ts.map +1 -1
- package/dist/mcp/tools/browser/element-info.js +24 -0
- package/dist/mcp/tools/browser/element-info.js.map +1 -1
- package/dist/vscode/vscode-tunnel-websocket.d.ts +76 -2
- package/dist/vscode/vscode-tunnel-websocket.d.ts.map +1 -1
- package/dist/vscode/vscode-tunnel-websocket.js +288 -11
- package/dist/vscode/vscode-tunnel-websocket.js.map +1 -1
- package/package.json +2 -1
|
@@ -6,7 +6,7 @@ import { BrowserSessionManager } from '../mcp/tools/browser/browser-session-mana
|
|
|
6
6
|
* API Server → Agent メッセージ
|
|
7
7
|
*/
|
|
8
8
|
export interface VsCodeServerMessage {
|
|
9
|
-
type: 'vscode_open' | 'vscode_close' | 'port_forward_open' | 'port_forward_close' | 'http_request' | 'ws_frame' | 'auth_success' | 'error' | 'browser_open' | 'browser_close' | 'browser_navigate' | 'browser_go_back' | 'browser_go_forward' | 'browser_reload' | 'browser_mouse_click' | 'browser_mouse_move' | 'browser_mouse_down' | 'browser_mouse_up' | 'browser_mouse_wheel' | 'browser_keyboard_type' | 'browser_keyboard_press' | 'browser_screenshot' | 'browser_viewport' | 'browser_execute_script' | 'browser_set_file' | 'browser_get_selection';
|
|
9
|
+
type: 'vscode_open' | 'vscode_close' | 'port_forward_open' | 'port_forward_close' | 'http_request' | 'ws_frame' | 'auth_success' | 'error' | 'browser_open' | 'browser_close' | 'browser_navigate' | 'browser_go_back' | 'browser_go_forward' | 'browser_reload' | 'browser_mouse_click' | 'browser_mouse_move' | 'browser_mouse_down' | 'browser_mouse_up' | 'browser_mouse_wheel' | 'browser_keyboard_type' | 'browser_keyboard_press' | 'browser_screenshot' | 'browser_viewport' | 'browser_execute_script' | 'browser_set_file' | 'browser_get_selection' | 'browser_set_input_value';
|
|
10
10
|
filePaths?: string[];
|
|
11
11
|
files?: Array<{
|
|
12
12
|
name: string;
|
|
@@ -41,12 +41,15 @@ export interface VsCodeServerMessage {
|
|
|
41
41
|
height?: number;
|
|
42
42
|
deviceId?: string;
|
|
43
43
|
script?: string;
|
|
44
|
+
value?: string;
|
|
45
|
+
selectionStart?: number;
|
|
46
|
+
selectionEnd?: number;
|
|
44
47
|
}
|
|
45
48
|
/**
|
|
46
49
|
* Agent → API Server メッセージ
|
|
47
50
|
*/
|
|
48
51
|
export interface VsCodeAgentMessage {
|
|
49
|
-
type: 'vscode_ready' | 'vscode_stopped' | 'port_forward_ready' | 'port_forward_stopped' | 'http_response' | 'ws_frame' | 'error' | 'browser_ready' | 'browser_frame' | 'browser_screenshot_result' | 'browser_selection_result' | 'browser_action_log' | 'browser_stopped' | 'browser_script_result' | 'browser_script_progress' | 'browser_file_chooser_opened';
|
|
52
|
+
type: 'vscode_ready' | 'vscode_stopped' | 'port_forward_ready' | 'port_forward_stopped' | 'http_response' | 'ws_frame' | 'error' | 'browser_ready' | 'browser_frame' | 'browser_screenshot_result' | 'browser_selection_result' | 'browser_action_log' | 'browser_stopped' | 'browser_script_result' | 'browser_script_progress' | 'browser_file_chooser_opened' | 'browser_cursor_update' | 'browser_focus_changed';
|
|
50
53
|
sessionId?: string;
|
|
51
54
|
targetPort?: number;
|
|
52
55
|
requestId?: string;
|
|
@@ -87,6 +90,26 @@ export interface VsCodeAgentMessage {
|
|
|
87
90
|
step?: number;
|
|
88
91
|
line?: string;
|
|
89
92
|
script?: string;
|
|
93
|
+
/** CSS cursor value at the last mouse-move point (browser_cursor_update) */
|
|
94
|
+
cursor?: string;
|
|
95
|
+
focused?: boolean;
|
|
96
|
+
rect?: {
|
|
97
|
+
x: number;
|
|
98
|
+
y: number;
|
|
99
|
+
width: number;
|
|
100
|
+
height: number;
|
|
101
|
+
};
|
|
102
|
+
value?: string;
|
|
103
|
+
selectionStart?: number;
|
|
104
|
+
selectionEnd?: number;
|
|
105
|
+
multiline?: boolean;
|
|
106
|
+
inputType?: string;
|
|
107
|
+
maxLength?: number;
|
|
108
|
+
fontSize?: number;
|
|
109
|
+
lineHeight?: number;
|
|
110
|
+
textAlign?: string;
|
|
111
|
+
paddingTop?: number;
|
|
112
|
+
paddingLeft?: number;
|
|
90
113
|
}
|
|
91
114
|
/**
|
|
92
115
|
* VS Code トンネル WebSocket
|
|
@@ -118,6 +141,11 @@ export declare class VsCodeTunnelWebSocket extends BaseWebSocketConnection<VsCod
|
|
|
118
141
|
private browserLocalServer;
|
|
119
142
|
private browserLocalPort;
|
|
120
143
|
private readonly pendingFileChoosers;
|
|
144
|
+
/**
|
|
145
|
+
* Last CSS cursor value sent per browser session. Used to suppress redundant
|
|
146
|
+
* browser_cursor_update messages: only send when the cursor shape changes.
|
|
147
|
+
*/
|
|
148
|
+
private readonly lastSentCursor;
|
|
121
149
|
constructor(apiUrl: string, token: string, agentId: string, projectDir?: string | undefined,
|
|
122
150
|
/**
|
|
123
151
|
* code-server セッション起動時に最新の envVars を取り出す関数。
|
|
@@ -169,9 +197,55 @@ export declare class VsCodeTunnelWebSocket extends BaseWebSocketConnection<VsCod
|
|
|
169
197
|
private handleBrowserKeyboardPress;
|
|
170
198
|
private handleBrowserScreenshot;
|
|
171
199
|
private handleBrowserGetSelection;
|
|
200
|
+
private handleBrowserSetInputValue;
|
|
172
201
|
private handleBrowserExecuteScript;
|
|
173
202
|
private handleBrowserViewport;
|
|
174
203
|
private handleBrowserSetFile;
|
|
204
|
+
/**
|
|
205
|
+
* Branch 1: Agent FS paths chosen directly by the user via the workspace file
|
|
206
|
+
* explorer. These arrive as workspace-relative paths (e.g. `repos/app.ts`) —
|
|
207
|
+
* Playwright's setFiles requires absolute paths, so each is resolved against
|
|
208
|
+
* the workspace root before being forwarded. Already absolute paths are
|
|
209
|
+
* accepted as-is for backward compatibility, but every resolved path must
|
|
210
|
+
* stay inside the workspace root (traversal + symlink-escape guard).
|
|
211
|
+
*/
|
|
212
|
+
private handleBrowserSetFileByPaths;
|
|
213
|
+
/**
|
|
214
|
+
* Branch 2: base64 file contents uploaded from the web client. Decode each
|
|
215
|
+
* into a temporary file on the agent host, hand the temp paths to setFiles,
|
|
216
|
+
* then always remove the temp files afterwards.
|
|
217
|
+
*/
|
|
218
|
+
private handleBrowserSetFileByContent;
|
|
219
|
+
/**
|
|
220
|
+
* Resolve user-chosen file-explorer paths into absolute paths suitable for
|
|
221
|
+
* Playwright's `setFiles`, enforcing that every resolved path stays inside
|
|
222
|
+
* the agent workspace root.
|
|
223
|
+
*
|
|
224
|
+
* Resolution rules per path:
|
|
225
|
+
* - Absolute paths are kept as-is (backward compatibility for callers that
|
|
226
|
+
* already send absolute agent-FS paths).
|
|
227
|
+
* - Relative paths (e.g. `repos/app.ts`) are resolved against the workspace
|
|
228
|
+
* directory (`getWorkspaceDir(projectDir)`).
|
|
229
|
+
*
|
|
230
|
+
* Traversal guard (lexical): after resolution, the absolute path must be the
|
|
231
|
+
* workspace root itself or a descendant of it. `path.relative(workspaceDir,
|
|
232
|
+
* resolved)` starting with `..` (or being absolute) means the path escaped the
|
|
233
|
+
* workspace and is rejected. This rejects both relative escapes
|
|
234
|
+
* (`../../etc/passwd`) and absolute paths pointing outside the workspace.
|
|
235
|
+
*
|
|
236
|
+
* Symlink-escape guard (physical): a path that is lexically inside the
|
|
237
|
+
* workspace may still resolve, through a symlink, to a target OUTSIDE it
|
|
238
|
+
* (e.g. a `link` inside the workspace pointing at `../../.ssh/id_rsa`). After
|
|
239
|
+
* the lexical check, each existing path's real (canonical) location is
|
|
240
|
+
* resolved via `fs.realpath` and re-checked against the workspace root. Paths
|
|
241
|
+
* whose real location escapes the workspace are rejected. Non-existent paths
|
|
242
|
+
* cannot be canonicalized; they are left as-is (Playwright's setFiles will
|
|
243
|
+
* fail on them) rather than rejected here, preserving the lexical guarantee.
|
|
244
|
+
*
|
|
245
|
+
* Returns the resolved absolute paths on success, or an error message string
|
|
246
|
+
* describing why the upload was rejected (never silently dropped).
|
|
247
|
+
*/
|
|
248
|
+
private resolveWorkspaceFilePaths;
|
|
175
249
|
/**
|
|
176
250
|
* Cancel a pending file chooser by applying an empty file list so the remote
|
|
177
251
|
* `<input type=file>` is cleared rather than left pending. Failures are
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vscode-tunnel-websocket.d.ts","sourceRoot":"","sources":["../../src/vscode/vscode-tunnel-websocket.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"vscode-tunnel-websocket.d.ts","sourceRoot":"","sources":["../../src/vscode/vscode-tunnel-websocket.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,14 +1,51 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
2
35
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
36
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
37
|
};
|
|
5
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
39
|
exports.VsCodeTunnelWebSocket = void 0;
|
|
40
|
+
const node_fs_1 = require("node:fs");
|
|
41
|
+
const os = __importStar(require("node:os"));
|
|
42
|
+
const path = __importStar(require("node:path"));
|
|
7
43
|
const ws_1 = __importDefault(require("ws"));
|
|
8
44
|
const base_websocket_1 = require("../base-websocket");
|
|
9
45
|
const browser_local_server_1 = require("../browser/browser-local-server");
|
|
10
46
|
const constants_1 = require("../constants");
|
|
11
47
|
const logger_1 = require("../logger");
|
|
48
|
+
const project_dir_1 = require("../project-dir");
|
|
12
49
|
const utils_1 = require("../utils");
|
|
13
50
|
const browser_session_manager_1 = require("../mcp/tools/browser/browser-session-manager");
|
|
14
51
|
const browser_security_1 = require("../mcp/tools/browser/browser-security");
|
|
@@ -24,6 +61,27 @@ const LIVE_VIEW_INTERVAL_MS = 200;
|
|
|
24
61
|
* Guards against unbounded base64 payloads consuming agent memory.
|
|
25
62
|
*/
|
|
26
63
|
const MAX_UPLOAD_BYTES = 10 * 1024 * 1024; // 10MB
|
|
64
|
+
/**
|
|
65
|
+
* Sanitize a client-supplied upload file name into a safe basename so it can be
|
|
66
|
+
* joined to a temp directory without escaping it (path-traversal defense).
|
|
67
|
+
*
|
|
68
|
+
* Strips any directory components (`path.basename`), then rejects path-relative
|
|
69
|
+
* tokens (`.`/`..`) and any remaining separator characters by replacing unsafe
|
|
70
|
+
* characters with `_`. Always returns a non-empty basename, falling back to
|
|
71
|
+
* `upload` when nothing safe remains.
|
|
72
|
+
*/
|
|
73
|
+
function sanitizeUploadFileName(name) {
|
|
74
|
+
const raw = typeof name === 'string' ? name : '';
|
|
75
|
+
// Drop any directory portion the client may have included.
|
|
76
|
+
let base = path.basename(raw);
|
|
77
|
+
// Replace path separators and traversal-relevant characters; collapse control
|
|
78
|
+
// chars and leading dots that could hide the file or break paths.
|
|
79
|
+
base = base.replace(/[/\\]/g, '_').replace(/\0/g, '');
|
|
80
|
+
if (base === '' || base === '.' || base === '..') {
|
|
81
|
+
return 'upload';
|
|
82
|
+
}
|
|
83
|
+
return base;
|
|
84
|
+
}
|
|
27
85
|
/**
|
|
28
86
|
* VS Code トンネル WebSocket
|
|
29
87
|
*
|
|
@@ -50,6 +108,11 @@ class VsCodeTunnelWebSocket extends base_websocket_1.BaseWebSocketConnection {
|
|
|
50
108
|
browserLocalServer = null;
|
|
51
109
|
browserLocalPort = 0;
|
|
52
110
|
pendingFileChoosers = new Map();
|
|
111
|
+
/**
|
|
112
|
+
* Last CSS cursor value sent per browser session. Used to suppress redundant
|
|
113
|
+
* browser_cursor_update messages: only send when the cursor shape changes.
|
|
114
|
+
*/
|
|
115
|
+
lastSentCursor = new Map();
|
|
53
116
|
constructor(apiUrl, token, agentId, projectDir,
|
|
54
117
|
/**
|
|
55
118
|
* code-server セッション起動時に最新の envVars を取り出す関数。
|
|
@@ -208,6 +271,9 @@ class VsCodeTunnelWebSocket extends base_websocket_1.BaseWebSocketConnection {
|
|
|
208
271
|
case 'browser_get_selection':
|
|
209
272
|
void this.handleBrowserGetSelection(msg);
|
|
210
273
|
break;
|
|
274
|
+
case 'browser_set_input_value':
|
|
275
|
+
void this.handleBrowserSetInputValue(msg);
|
|
276
|
+
break;
|
|
211
277
|
default:
|
|
212
278
|
logger_1.logger.debug(`[vscode-ws] Unknown message type: ${msg.type}`);
|
|
213
279
|
}
|
|
@@ -452,6 +518,14 @@ class VsCodeTunnelWebSocket extends base_websocket_1.BaseWebSocketConnection {
|
|
|
452
518
|
this.sendMissingSessionIdError();
|
|
453
519
|
return;
|
|
454
520
|
}
|
|
521
|
+
// Reset any stale cursor state for this sessionId on (re)open. The idle-timeout
|
|
522
|
+
// auto-close path closes a session via BrowserSessionManager WITHOUT routing
|
|
523
|
+
// through handleBrowserClose/cleanup, so lastSentCursor can survive a closed
|
|
524
|
+
// session. If the same sessionId is later re-opened, a stale entry would make
|
|
525
|
+
// the first mouse-move's cursor look "unchanged" and suppress the initial
|
|
526
|
+
// browser_cursor_update, freezing the client cursor at the previous session's
|
|
527
|
+
// last shape. Deleting here guarantees the first move after (re)open is sent.
|
|
528
|
+
this.lastSentCursor.delete(sessionId);
|
|
455
529
|
try {
|
|
456
530
|
const session = await this.browserSessionManager.getOrCreate(sessionId);
|
|
457
531
|
// Wire up action log notifications so in-process operations are relayed to Web UI
|
|
@@ -467,6 +541,11 @@ class VsCodeTunnelWebSocket extends base_websocket_1.BaseWebSocketConnection {
|
|
|
467
541
|
this.pendingFileChoosers.set(sessionId, accept);
|
|
468
542
|
this.send({ type: 'browser_file_chooser_opened', sessionId });
|
|
469
543
|
};
|
|
544
|
+
// Wire up focus-change notifications so the Web client can overlay a real
|
|
545
|
+
// input/textarea on the focused element (native caret + IME).
|
|
546
|
+
session.onFocusChange = (payload) => {
|
|
547
|
+
this.send({ type: 'browser_focus_changed', sessionId, ...payload });
|
|
548
|
+
};
|
|
470
549
|
if (msg.conversationId) {
|
|
471
550
|
this.browserSessionManager.linkConversation(msg.conversationId, sessionId);
|
|
472
551
|
}
|
|
@@ -523,6 +602,7 @@ class VsCodeTunnelWebSocket extends base_websocket_1.BaseWebSocketConnection {
|
|
|
523
602
|
await this.browserSessionManager.close(sessionId);
|
|
524
603
|
}
|
|
525
604
|
this.pendingFileChoosers.delete(sessionId);
|
|
605
|
+
this.lastSentCursor.delete(sessionId);
|
|
526
606
|
this.send({ type: 'browser_stopped', sessionId, reason: 'closed' });
|
|
527
607
|
logger_1.logger.info(`[vscode-ws] Browser session closed: ${sessionId}`);
|
|
528
608
|
}
|
|
@@ -598,8 +678,9 @@ class VsCodeTunnelWebSocket extends base_websocket_1.BaseWebSocketConnection {
|
|
|
598
678
|
}
|
|
599
679
|
}
|
|
600
680
|
async handleBrowserMouseMove(msg) {
|
|
601
|
-
const
|
|
602
|
-
|
|
681
|
+
const sessionId = msg.sessionId;
|
|
682
|
+
const session = sessionId ? this.browserSessionManager.get(sessionId) : undefined;
|
|
683
|
+
if (!session || !sessionId || msg.x === undefined || msg.y === undefined)
|
|
603
684
|
return;
|
|
604
685
|
try {
|
|
605
686
|
await session.executeMouseMove(msg.x, msg.y);
|
|
@@ -609,7 +690,24 @@ class VsCodeTunnelWebSocket extends base_websocket_1.BaseWebSocketConnection {
|
|
|
609
690
|
// navigation) is intentionally logged but NOT surfaced as an `error` message
|
|
610
691
|
// to the web client, to avoid error spam during drag-selection. Unlike
|
|
611
692
|
// mouse_down / mouse_up, a dropped move does not leave the page in a broken state.
|
|
612
|
-
logger_1.logger.warn(`[vscode-ws] mouseMove failed (session=${
|
|
693
|
+
logger_1.logger.warn(`[vscode-ws] mouseMove failed (session=${sessionId}): ${(0, utils_1.getErrorMessage)(error)}`);
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
// Best-effort cursor-shape mirroring: read the CSS cursor at the point and,
|
|
697
|
+
// only when it differs from the last value sent for this session, forward a
|
|
698
|
+
// browser_cursor_update. A failed read is logged (not silently swallowed)
|
|
699
|
+
// and skips the update for this frame rather than spamming an error message.
|
|
700
|
+
let cursor;
|
|
701
|
+
try {
|
|
702
|
+
cursor = await session.getCursorAt(msg.x, msg.y);
|
|
703
|
+
}
|
|
704
|
+
catch (error) {
|
|
705
|
+
logger_1.logger.warn(`[vscode-ws] getCursorAt failed (session=${sessionId}): ${(0, utils_1.getErrorMessage)(error)}`);
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
if (this.lastSentCursor.get(sessionId) !== cursor) {
|
|
709
|
+
this.lastSentCursor.set(sessionId, cursor);
|
|
710
|
+
this.send({ type: 'browser_cursor_update', sessionId, cursor });
|
|
613
711
|
}
|
|
614
712
|
}
|
|
615
713
|
async handleBrowserMouseDown(msg) {
|
|
@@ -715,6 +813,21 @@ class VsCodeTunnelWebSocket extends base_websocket_1.BaseWebSocketConnection {
|
|
|
715
813
|
this.send({ type: 'error', sessionId, message: `getSelection failed: ${(0, utils_1.getErrorMessage)(error)}` });
|
|
716
814
|
}
|
|
717
815
|
}
|
|
816
|
+
async handleBrowserSetInputValue(msg) {
|
|
817
|
+
const session = msg.sessionId ? this.browserSessionManager.get(msg.sessionId) : undefined;
|
|
818
|
+
if (!session)
|
|
819
|
+
return;
|
|
820
|
+
// The contract requires `value` to be a string; ignore malformed payloads.
|
|
821
|
+
if (typeof msg.value !== 'string')
|
|
822
|
+
return;
|
|
823
|
+
try {
|
|
824
|
+
await session.setFocusedInputValue(msg.value, msg.selectionStart, msg.selectionEnd);
|
|
825
|
+
}
|
|
826
|
+
catch (error) {
|
|
827
|
+
logger_1.logger.warn(`[vscode-ws] setInputValue failed (session=${msg.sessionId}): ${(0, utils_1.getErrorMessage)(error)}`);
|
|
828
|
+
this.send({ type: 'error', sessionId: msg.sessionId, message: `setInputValue failed: ${(0, utils_1.getErrorMessage)(error)}` });
|
|
829
|
+
}
|
|
830
|
+
}
|
|
718
831
|
async handleBrowserExecuteScript(msg) {
|
|
719
832
|
const sessionId = msg.sessionId;
|
|
720
833
|
if (!sessionId || !msg.script) {
|
|
@@ -773,34 +886,197 @@ class VsCodeTunnelWebSocket extends base_websocket_1.BaseWebSocketConnection {
|
|
|
773
886
|
}
|
|
774
887
|
// Consume the pending chooser up-front so it is not left dangling on any path.
|
|
775
888
|
this.pendingFileChoosers.delete(sessionId);
|
|
776
|
-
|
|
889
|
+
// Outermost guard: any unexpected error (malformed payload, FS failure, a
|
|
890
|
+
// throwing `accept`) MUST surface as an `error` notification AND cancel the
|
|
891
|
+
// chooser, never escape as an unhandled rejection that leaves the remote
|
|
892
|
+
// `<input type=file>` stuck waiting. Each branch below already cancels on
|
|
893
|
+
// its own validation failures; this catch is the final backstop so a
|
|
894
|
+
// non-string filePath element, a non-array `files`, etc. cannot wedge the
|
|
895
|
+
// chooser.
|
|
896
|
+
try {
|
|
897
|
+
if (msg.files === undefined && msg.filePaths !== undefined) {
|
|
898
|
+
await this.handleBrowserSetFileByPaths(sessionId, accept, msg.filePaths);
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
901
|
+
await this.handleBrowserSetFileByContent(sessionId, accept, msg.files);
|
|
902
|
+
}
|
|
903
|
+
catch (error) {
|
|
904
|
+
this.send({ type: 'error', sessionId, message: `File upload failed: ${(0, utils_1.getErrorMessage)(error)}` });
|
|
905
|
+
await this.cancelFileChooser(accept, sessionId);
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
/**
|
|
909
|
+
* Branch 1: Agent FS paths chosen directly by the user via the workspace file
|
|
910
|
+
* explorer. These arrive as workspace-relative paths (e.g. `repos/app.ts`) —
|
|
911
|
+
* Playwright's setFiles requires absolute paths, so each is resolved against
|
|
912
|
+
* the workspace root before being forwarded. Already absolute paths are
|
|
913
|
+
* accepted as-is for backward compatibility, but every resolved path must
|
|
914
|
+
* stay inside the workspace root (traversal + symlink-escape guard).
|
|
915
|
+
*/
|
|
916
|
+
async handleBrowserSetFileByPaths(sessionId, accept, filePaths) {
|
|
917
|
+
// Explicit input validation (do not rely on a thrown TypeError): the payload
|
|
918
|
+
// must be an array of strings. Anything else is rejected + chooser cancelled.
|
|
919
|
+
if (!Array.isArray(filePaths) || filePaths.some((p) => typeof p !== 'string')) {
|
|
920
|
+
this.send({
|
|
921
|
+
type: 'error',
|
|
922
|
+
sessionId,
|
|
923
|
+
message: 'Invalid file payload: filePaths must be an array of strings',
|
|
924
|
+
});
|
|
925
|
+
await this.cancelFileChooser(accept, sessionId);
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
928
|
+
const resolved = await this.resolveWorkspaceFilePaths(filePaths);
|
|
929
|
+
if (typeof resolved === 'string') {
|
|
930
|
+
// Resolution rejected the upload (escape attempt / missing workspace /
|
|
931
|
+
// symlink pointing outside). Surface the reason and cancel the chooser so
|
|
932
|
+
// the remote input is not left pending.
|
|
933
|
+
this.send({ type: 'error', sessionId, message: resolved });
|
|
934
|
+
await this.cancelFileChooser(accept, sessionId);
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
937
|
+
try {
|
|
938
|
+
await accept(resolved);
|
|
939
|
+
}
|
|
940
|
+
catch (error) {
|
|
941
|
+
// Symmetry with the base64 branch: when setFiles rejects, surface the
|
|
942
|
+
// error AND cancel the chooser so the remote input is not left pending.
|
|
943
|
+
this.send({ type: 'error', sessionId, message: `File upload failed: ${(0, utils_1.getErrorMessage)(error)}` });
|
|
944
|
+
await this.cancelFileChooser(accept, sessionId);
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
/**
|
|
948
|
+
* Branch 2: base64 file contents uploaded from the web client. Decode each
|
|
949
|
+
* into a temporary file on the agent host, hand the temp paths to setFiles,
|
|
950
|
+
* then always remove the temp files afterwards.
|
|
951
|
+
*/
|
|
952
|
+
async handleBrowserSetFileByContent(sessionId, accept, rawFiles) {
|
|
953
|
+
// Explicit input validation (do not rely on a thrown TypeError): `files`
|
|
954
|
+
// must be an array, and every entry must carry a string `dataBase64`.
|
|
955
|
+
if (rawFiles !== undefined && !Array.isArray(rawFiles)) {
|
|
956
|
+
this.send({ type: 'error', sessionId, message: 'Invalid file payload: files must be an array' });
|
|
957
|
+
await this.cancelFileChooser(accept, sessionId);
|
|
958
|
+
return;
|
|
959
|
+
}
|
|
960
|
+
const files = (rawFiles ?? []);
|
|
777
961
|
// Validate every entry has a string dataBase64 before decoding so we never
|
|
778
962
|
// pass undefined / non-string into Buffer.from.
|
|
779
|
-
if (files.some((f) => typeof f
|
|
963
|
+
if (files.some((f) => typeof f?.dataBase64 !== 'string')) {
|
|
780
964
|
this.send({ type: 'error', sessionId, message: 'Invalid file payload: dataBase64 must be a string' });
|
|
781
965
|
// Cancel the remote input so it is not left pending/half-filled.
|
|
782
966
|
await this.cancelFileChooser(accept, sessionId);
|
|
783
967
|
return;
|
|
784
968
|
}
|
|
969
|
+
const safeFiles = files;
|
|
785
970
|
// Estimate decoded size from base64 length and enforce the authoritative limit
|
|
786
971
|
// before allocating buffers.
|
|
787
|
-
const totalBytes =
|
|
972
|
+
const totalBytes = safeFiles.reduce((sum, f) => sum + Math.floor((f.dataBase64.length * 3) / 4), 0);
|
|
788
973
|
if (totalBytes > MAX_UPLOAD_BYTES) {
|
|
789
974
|
this.send({ type: 'error', sessionId, message: 'File too large (max 10MB)' });
|
|
790
975
|
await this.cancelFileChooser(accept, sessionId);
|
|
791
976
|
return;
|
|
792
977
|
}
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
978
|
+
// Create a unique temp directory for this upload so concurrent uploads and
|
|
979
|
+
// duplicate file names never collide.
|
|
980
|
+
let tmpDir;
|
|
981
|
+
try {
|
|
982
|
+
tmpDir = await node_fs_1.promises.mkdtemp(path.join(os.tmpdir(), 'browser-upload-'));
|
|
983
|
+
}
|
|
984
|
+
catch (error) {
|
|
985
|
+
this.send({ type: 'error', sessionId, message: `File upload failed: ${(0, utils_1.getErrorMessage)(error)}` });
|
|
986
|
+
await this.cancelFileChooser(accept, sessionId);
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
798
989
|
try {
|
|
990
|
+
const tmpPaths = [];
|
|
991
|
+
for (let i = 0; i < safeFiles.length; i++) {
|
|
992
|
+
const file = safeFiles[i];
|
|
993
|
+
// Prefix with the index so distinct uploads sharing a sanitized name
|
|
994
|
+
// never overwrite one another within the temp dir.
|
|
995
|
+
const safeName = `${i}-${sanitizeUploadFileName(file.name)}`;
|
|
996
|
+
const tmpPath = path.join(tmpDir, safeName);
|
|
997
|
+
await node_fs_1.promises.writeFile(tmpPath, Buffer.from(file.dataBase64, 'base64'));
|
|
998
|
+
tmpPaths.push(tmpPath);
|
|
999
|
+
}
|
|
1000
|
+
const payload = tmpPaths;
|
|
799
1001
|
await accept(payload);
|
|
800
1002
|
}
|
|
801
1003
|
catch (error) {
|
|
802
1004
|
this.send({ type: 'error', sessionId, message: `File upload failed: ${(0, utils_1.getErrorMessage)(error)}` });
|
|
1005
|
+
await this.cancelFileChooser(accept, sessionId);
|
|
1006
|
+
}
|
|
1007
|
+
finally {
|
|
1008
|
+
// Always remove the temp files/dir once setFiles has resolved or rejected.
|
|
1009
|
+
await node_fs_1.promises.rm(tmpDir, { recursive: true, force: true }).catch((error) => {
|
|
1010
|
+
logger_1.logger.warn(`[vscode-ws] failed to remove temp upload dir ${tmpDir}: ${(0, utils_1.getErrorMessage)(error)}`);
|
|
1011
|
+
});
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
/**
|
|
1015
|
+
* Resolve user-chosen file-explorer paths into absolute paths suitable for
|
|
1016
|
+
* Playwright's `setFiles`, enforcing that every resolved path stays inside
|
|
1017
|
+
* the agent workspace root.
|
|
1018
|
+
*
|
|
1019
|
+
* Resolution rules per path:
|
|
1020
|
+
* - Absolute paths are kept as-is (backward compatibility for callers that
|
|
1021
|
+
* already send absolute agent-FS paths).
|
|
1022
|
+
* - Relative paths (e.g. `repos/app.ts`) are resolved against the workspace
|
|
1023
|
+
* directory (`getWorkspaceDir(projectDir)`).
|
|
1024
|
+
*
|
|
1025
|
+
* Traversal guard (lexical): after resolution, the absolute path must be the
|
|
1026
|
+
* workspace root itself or a descendant of it. `path.relative(workspaceDir,
|
|
1027
|
+
* resolved)` starting with `..` (or being absolute) means the path escaped the
|
|
1028
|
+
* workspace and is rejected. This rejects both relative escapes
|
|
1029
|
+
* (`../../etc/passwd`) and absolute paths pointing outside the workspace.
|
|
1030
|
+
*
|
|
1031
|
+
* Symlink-escape guard (physical): a path that is lexically inside the
|
|
1032
|
+
* workspace may still resolve, through a symlink, to a target OUTSIDE it
|
|
1033
|
+
* (e.g. a `link` inside the workspace pointing at `../../.ssh/id_rsa`). After
|
|
1034
|
+
* the lexical check, each existing path's real (canonical) location is
|
|
1035
|
+
* resolved via `fs.realpath` and re-checked against the workspace root. Paths
|
|
1036
|
+
* whose real location escapes the workspace are rejected. Non-existent paths
|
|
1037
|
+
* cannot be canonicalized; they are left as-is (Playwright's setFiles will
|
|
1038
|
+
* fail on them) rather than rejected here, preserving the lexical guarantee.
|
|
1039
|
+
*
|
|
1040
|
+
* Returns the resolved absolute paths on success, or an error message string
|
|
1041
|
+
* describing why the upload was rejected (never silently dropped).
|
|
1042
|
+
*/
|
|
1043
|
+
async resolveWorkspaceFilePaths(filePaths) {
|
|
1044
|
+
// A workspace root is required to both resolve relative paths and to bound
|
|
1045
|
+
// absolute ones. Without a project directory we cannot safely accept any
|
|
1046
|
+
// file-explorer path.
|
|
1047
|
+
if (!this.projectDir) {
|
|
1048
|
+
return 'Cannot resolve file paths: no project directory configured for this agent';
|
|
1049
|
+
}
|
|
1050
|
+
const workspaceDir = path.resolve((0, project_dir_1.getWorkspaceDir)(this.projectDir));
|
|
1051
|
+
// Canonicalize the workspace root too, so a symlinked workspace dir does not
|
|
1052
|
+
// produce false positives when comparing against realpath'd children.
|
|
1053
|
+
const realWorkspaceDir = await node_fs_1.promises.realpath(workspaceDir).catch(() => workspaceDir);
|
|
1054
|
+
const isInside = (root, target) => {
|
|
1055
|
+
const rel = path.relative(root, target);
|
|
1056
|
+
// Inside when the relative path stays under root: not climbing out (`..`)
|
|
1057
|
+
// and not forced absolute (different root/drive). An empty `rel` means the
|
|
1058
|
+
// path IS the root, which is allowed.
|
|
1059
|
+
return !rel.startsWith('..') && !path.isAbsolute(rel);
|
|
1060
|
+
};
|
|
1061
|
+
const resolved = [];
|
|
1062
|
+
for (const filePath of filePaths) {
|
|
1063
|
+
const absolute = path.isAbsolute(filePath)
|
|
1064
|
+
? path.resolve(filePath)
|
|
1065
|
+
: path.resolve(workspaceDir, filePath);
|
|
1066
|
+
// Lexical guard first (cheap, also covers non-existent paths).
|
|
1067
|
+
if (!isInside(workspaceDir, absolute)) {
|
|
1068
|
+
return `Access denied: file path is outside the workspace: ${filePath}`;
|
|
1069
|
+
}
|
|
1070
|
+
// Physical guard: resolve symlinks and re-check. Missing paths cannot be
|
|
1071
|
+
// canonicalized — keep the lexically-validated absolute path (setFiles
|
|
1072
|
+
// will surface the non-existence downstream).
|
|
1073
|
+
const real = await node_fs_1.promises.realpath(absolute).catch(() => null);
|
|
1074
|
+
if (real !== null && !isInside(realWorkspaceDir, real)) {
|
|
1075
|
+
return `Access denied: file path is outside the workspace: ${filePath}`;
|
|
1076
|
+
}
|
|
1077
|
+
resolved.push(absolute);
|
|
803
1078
|
}
|
|
1079
|
+
return resolved;
|
|
804
1080
|
}
|
|
805
1081
|
/**
|
|
806
1082
|
* Cancel a pending file chooser by applying an empty file list so the remote
|
|
@@ -820,6 +1096,7 @@ class VsCodeTunnelWebSocket extends base_websocket_1.BaseWebSocketConnection {
|
|
|
820
1096
|
// ブラウザセッションのクリーンアップ
|
|
821
1097
|
void this.browserSessionManager.closeAll();
|
|
822
1098
|
this.pendingFileChoosers.clear();
|
|
1099
|
+
this.lastSentCursor.clear();
|
|
823
1100
|
// ブラウザローカルサーバーのクリーンアップ
|
|
824
1101
|
if (this.browserLocalServer) {
|
|
825
1102
|
void this.browserLocalServer.stop();
|