sshRemoteAgentHostHelpers.ts ×20

Frontier kind: Code frontier

unlabeled · c_ef7b77657513

180 tests · 4085 LOC · 23 files · introduces 0 tests · 256 LOC · 2 files

Introduces — evidence that enters the hierarchy at this concept

Code
22 ranges256 lines · 2 files
Tests
0 tests

Contains — complete concept membership

All code (extent)
509 ranges4085 lines · 23 files · Browse complete extent
All tests (intent)
180 testsBrowse complete intent

Neighbourhood graph

The orange circle is the focus. Violet and green circles are every ancestor and descendant, broader and narrower, at any distance; blue squares and pink diamonds are the introduced files and exact introduced tests of every visible concept, not only the focus's. Arrows point from broader to narrower concepts and bridge only concepts omitted from this view. Undirected links show source or test introduction. Concept and file size follows LOC; exact test nodes use test-count units.

Introduced files, introduced tests, and structurally relevant concept specialization

In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the native relationship evidence on this page.

Graph controls are ready.

Interactive rendering requires JavaScript and WebGL. Use the native relationship evidence on this page while the interactive map is unavailable.

Native relationship evidence

Every exact file and test below is linked only from the concept that introduces it.

Introduced tests

Every collected test enters the hierarchy at exactly one concept.

No tests are introduced at this concept. Its intent tests are introduced by other concepts.

Introduced code

Every collected source range enters the hierarchy at exactly one concept.

2 files ranked by introduced lines: 256 introduced LOC across 22 ranges. Expand a file to inspect source; the > gutter marks introduced lines.

src/vs/platform/agentHost/node/sshRemoteAgentHostHelpers.ts 220 introduced LOC · 20 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- sshRemoteAgentHostHelpers.ts
2 > * Copyright (c) Microsoft Corporation. All rights reserved.
3 > * Licensed under the MIT License. See License.txt in the project root for license information.
4 > *--------------------------------------------------------------------------------------------*/
5 >
6 > import { ILogService } from '../../log/common/log.js';
7 > import { createRemoteAgentHostState, parseRemoteAgentHostState } from '../common/remoteAgentHostMetadata.js';
8 >
9 > const LOG_PREFIX = '[SSHRemoteAgentHost]';
10 >
11 > /**
12 > * Validate that a quality string is safe for bare interpolation in shell commands.
13 > * Quality comes from `productService.quality` (not user input) but we validate
14 > * as defense-in-depth since these values end up in unquoted shell paths (the `~`
15 > * prefix requires shell expansion, so we cannot single-quote the entire path).
16 > */
17 > export function validateShellToken(value: string, label: string): string {
18 if (!/^[a-zA-Z0-9._-]+$/.test(value)) {
19 throw new Error(`Unsafe ${label} value for shell interpolation: ${JSON.stringify(value)}`);
21 return value;
22 }
24 > /**
25 > * Validate and normalize a commit SHA. Returns the lowercase form.
26 > *
27 > * The commit-keyed install layout, the cleanup glob (`[0-9a-f]{40}`), and
28 > * the fallback discovery glob all assume an exactly-40-char lowercase hex
29 > * commit. If a caller ever supplies a non-SHA value (or uppercase hex),
30 > * the cleanup pass would silently miss those binaries and the
31 > * commit-pinned download URL could 404. Enforce the shape at the source.
32 > *
33 > * `productService.commit` is already lowercase hex in practice; the
34 > * normalization is defense-in-depth for any future callers.
35 > */
36 > export function validateCommit(commit: string): string {
37 const normalized = commit.toLowerCase();
38 if (!/^[0-9a-f]{40}$/.test(normalized)) {
41 return normalized;
42 }
44 > /**
45 > * Name of the CLI binary as it appears inside the downloaded archive,
46 > * derived from product quality. Matches the names used by Remote-SSH's
47 > * exec-server installer so that CLI binaries can be shared between the
48 > * two features.
49 > */
50 > export function getRemoteCLIArchiveName(quality: string): string {
51 const q = validateShellToken(quality, 'quality');
52 switch (q) {
56 }
57 }
59 > /**
60 > * Install root for the VS Code CLI on the remote machine. Shared with
61 > * Remote-SSH's exec-server installer so the two features can reuse each
62 > * other's installations. Also the parent of the agent host lockfile dir.
63 > */
64 > export function getRemoteCLIInstallRoot(serverDataFolderName: string): string {
65 const d = validateShellToken(serverDataFolderName, 'server data folder name');
66 return `~/${d}`;
67 }
69 > /**
70 > * Per-machine launcher data dir for `code agent host` (and the embedded
71 > * CLI machinery it inherits). Passed as `--cli-data-dir` so the CLI's
72 > * downloads cache, unpacked server installs, supervisor logs, and other
73 > * launcher state land under the same root Remote-SSH's `command-shell`
74 > * uses (e.g. `~/.vscode-server/cli`). Without this flag the CLI would
75 > * default to `~/.vscode-cli{,-<quality>}/` and split state across two
76 > * roots.
77 > *
78 > * The lockfile is unaffected: the Rust CLI anchors it on
79 > * `serverDataFolderName` regardless of `--cli-data-dir` (see
80 > * `cli/src/state.rs::agent_host_root`).
81 > */
82 > export function getRemoteCLIDataDir(serverDataFolderName: string): string {
83 return `${getRemoteCLIInstallRoot(serverDataFolderName)}/cli`;
84 }
86 > /**
87 > * Full path to the installed CLI binary on the remote.
88 > *
89 > * When `commit` is provided, the path is keyed on commit (e.g.
90 > * `~/.vscode-server/code-insiders-<40hex>`) so we can install the CLI
91 > * matching the current desktop without disturbing other installs. This
92 > * mirrors Remote-SSH's exec-server layout.
93 > *
94 > * When `commit` is undefined (dev/OSS builds with no commit in product
95 > * metadata), the path is just `<root>/<archive>` — a single, non-keyed
96 > * filename. Caller code should keep the loose `--version`-based reuse
97 > * check in that case.
98 > */
99 > export function getRemoteCLIBin(serverDataFolderName: string, quality: string, commit?: string): string {
100 const archive = getRemoteCLIArchiveName(quality);
101 const root = getRemoteCLIInstallRoot(serverDataFolderName);
106 return `${root}/${archive}`;
107 }
109 > /** Escape a string for use as a single shell argument (single-quote wrapping). */
110 > export function shellEscape(s: string): string {
111 // Wrap in single quotes; escape embedded single quotes as: '\''
112 const escaped = s.replace(/'/g, '\'\\\'\'');
113 return `'${escaped}'`;
114 }
116 > /**
117 > * Construct the bare command that launches the agent host on the remote.
118 > *
119 > * `--cli-data-dir` is passed up-front so the embedded CLI's launcher
120 > * state (downloads cache, unpacked server installs, supervisor log) lands
121 > * under `<cliDataDir>` — the same root Remote-SSH uses. The supervisor
122 > * propagates this flag to its detached child (see
123 > * `cli/src/commands/agent_host.rs`), so the entire AH process tree
124 > * agrees on one launcher root.
125 > *
126 > * Inputs must already be safe for unquoted shell interpolation; callers
127 > * build them via {@link getRemoteCLIBin} / {@link getRemoteCLIDataDir}
128 > * which validate their components.
129 > */
130 > export function buildAgentHostBaseCommand(cliBin: string, cliDataDir: string): string {
131 return `${cliBin} --cli-data-dir ${cliDataDir} agent host --port 0`;
132 }
134 > export function resolveRemotePlatform(unameS: string, unameM: string): { os: string; arch: string } | undefined {
135 const os = unameS.trim().toLowerCase();
136 const machine = unameM.trim().toLowerCase();
158 return { os: platformOs, arch };
159 }
161 > /**
162 > * URL of the CLI download artifact.
163 > *
164 > * When `commit` is provided, uses the commit-pinned URL form so we get
165 > * the exact CLI matching the current desktop build (mirrors Remote-SSH).
166 > * When `commit` is undefined (dev/OSS builds), falls back to `latest`.
167 > */
168 > export function buildCLIDownloadUrl(os: string, arch: string, quality: string, commit?: string): string {
169 const base = 'https://update.code.visualstudio.com';
170 const artifact = `cli-${os}-${arch}`;
178 return `${base}/latest/${artifact}/${quality}`;
179 }
181 > /**
182 > * Shell snippet that prunes older commit-keyed CLI binaries from the
183 > * install root, keeping the 5 most recently modified. Mirrors the
184 > * retention policy in Remote-SSH's exec-server installer.
185 > *
186 > * The glob is tightened to exactly 40 hex chars (`[0-9a-f]`-only) so we
187 > * never accidentally delete (or hand to `xargs`) any filename that
188 > * happens to start with `<archive>-` but isn't actually one of our
189 > * commit-keyed binaries — both for correctness and to avoid passing
190 > * attacker-controlled filenames through `xargs rm` with option/whitespace
191 > * splitting hazards. We also use `rm -f --` and `xargs -I{}` (which
192 > * skips the command entirely on empty input on both GNU and BSD `xargs`).
193 > */
194 > export function buildCleanupOldCLIsCommand(serverDataFolderName: string, quality: string): string {
195 const root = getRemoteCLIInstallRoot(serverDataFolderName);
196 const archive = getRemoteCLIArchiveName(quality);
203 return `ls -1t -- ${root}/${archive}-${commitGlob} 2>/dev/null | awk 'NR>5' | xargs -I{} rm -f -- {} 2>/dev/null; true`;
204 }
206 > /**
207 > * Shell snippet that prints candidate CLI binary paths that could be
208 > * used as a fallback when the commit-pinned download fails. Order: any
209 > * commit-keyed binaries in the shared install root (newest mtime first),
210 > * then the legacy single-binary paths from the previous installer
211 > * (`~/.vscode-cli{,-<quality>}/<archive>`).
212 > *
213 > * Each line is a single path. The glob for commit-keyed candidates is
214 > * restricted to exactly 40 hex chars so the output can only contain
215 > * filenames we recognise (callers should still re-validate with
216 > * {@link isValidFallbackCLIPath}). The legacy paths are fixed strings
217 > * derived from validated tokens, so they cannot contain shell
218 > * metacharacters either.
219 > */
220 > export function buildFindFallbackCLICommand(serverDataFolderName: string, quality: string): string {
221 const root = getRemoteCLIInstallRoot(serverDataFolderName);
222 const archive = getRemoteCLIArchiveName(quality);
231 ].join('; ');
232 }
234 > /**
235 > * Validate that a candidate path string returned by the remote shell
236 > * matches one of the two shapes we expect from
237 > * {@link buildFindFallbackCLICommand}:
238 > *
239 > * - `<installRoot>/<archive>-<40 hex chars>` — commit-keyed install
240 > * - `<legacyDir>/<archive>` — legacy single-binary install
241 > *
242 > * Anything else is rejected. This guards against the candidate being
243 > * interpolated into a follow-up shell command (`<candidate> --version`,
244 > * agent host spawn) with attacker-controlled metacharacters in the
245 > * event that something unexpected ends up in the install root.
246 > */
247 > export function isValidFallbackCLIPath(candidate: string, serverDataFolderName: string, quality: string): boolean {
248 const root = getRemoteCLIInstallRoot(serverDataFolderName);
249 const archive = getRemoteCLIArchiveName(quality);
261 return false;
262 }
264 > /** Redact connection tokens from log output. */
265 > export function redactToken(text: string): string {
266 return text.replace(/\?tkn=[^\s&]+/g, '?tkn=***');
267 }
269 > /**
270 > * Match the `ws://127.0.0.1:PORT[?tkn=TOKEN]` URL emitted by `code agent host`
271 > * on stdout/stderr. Shared by SSH and WSL agent-host transports — both spawn
272 > * the CLI inside a posix shell and scrape its first line of output to discover
273 > * the WebSocket endpoint.
274 > */
275 > const AGENT_HOST_WS_URL_RE = /ws:\/\/(?:127\.0\.0\.1|localhost):(\d+)(?:\?tkn=([^\s&]+))?/;
276 >
277 > /**
278 > * Extract the `ws://` URL printed by `code agent host` from a line or buffer
279 > * of mixed output. Returns the full URL plus its parsed components, or
280 > * `undefined` if no match is found.
281 > */
282 > export function extractAgentHostWebSocketURL(text: string): { url: string; host: string; port: number; token: string | undefined } | undefined {
283 const match = text.match(AGENT_HOST_WS_URL_RE);
284 if (!match) {
292 };
293 }
295 > /**
296 > * Path to the per-quality agent host lockfile written by `code agent host`.
297 > *
298 > * Mirrors the Rust CLI's launcher path layout (see
299 > * `cli/src/state.rs::agent_host_root`). The Rust CLI anchors the agent host
300 > * lockfile on `serverDataFolderName` (exposed to the CLI build as
301 > * `VSCODE_CLI_SERVER_DATA_FOLDER_NAME`, derived from
302 > * `IProductConfiguration.serverDataFolderName`) so a `code agent host`
303 > * started locally and the supervisor spawned by the SSH `command-shell`
304 > * path agree on the same lockfile regardless of `--cli-data-dir`.
305 > */
306 > export function getAgentHostLockfile(serverDataFolderName: string, quality: string): string {
307 const d = validateShellToken(serverDataFolderName, 'server data folder name');
308 const q = validateShellToken(quality, 'quality');
309 return `~/${d}/cli/agent-host-${q}.lock`;
310 }
312 > /**
313 > * Abstraction over SSH command execution to enable testing without a real SSH connection.
314 > */
315 > export interface ISshExec {
316 > (command: string, opts?: { ignoreExitCode?: boolean }): Promise<{ stdout: string; stderr: string; code: number }>;
317 > }
318 >
319 > export type FindRunningAgentHostResult =
320 > | { readonly kind: 'notFound' }
321 > | { readonly kind: 'compatible'; readonly host: string; readonly port: number; readonly connectionToken: string | undefined };
322 >
323 > /**
324 > * Try to find a running agent host on the remote by reading the lockfile and
325 > * verifying the recorded PID is still alive.
326 > */
327 export async function findRunningAgentHost(
328 exec: ISshExec,
373 };
374 }
376 > /**
377 > * Map a recorded `host` value from the agent host lockfile to a dialable
378 > * loopback address. The supervisor records the literal `--host` value it
379 > * was given (e.g. `0.0.0.0`, `::1`, `localhost`); local callers (SSH
380 > * relay, tunnel reuse-forward, renderer bridge) want a target they can
381 > * actually open a socket to. Wildcards are mapped to their corresponding
382 > * loopback; specific hosts pass through unchanged. Missing `host`
383 > * (lockfile written by an older CLI) falls back to IPv4 loopback to
384 > * preserve the prior behaviour.
385 > */
386 > export function dialAgentHostHost(bound: string | undefined): string {
387 if (!bound || bound === '0.0.0.0' || bound === '::' || bound === '[::]') {
388 return '127.0.0.1';
390 return bound;
391 }
393 > /**
394 > * After starting an agent host, record its PID/port/token in the lockfile on
395 > * the remote so that future connections can reuse the process.
396 > */
397 export async function writeAgentHostState(
398 exec: ISshExec,
424 logService.info(`${LOG_PREFIX} Wrote agent host state to ${stateFile}: PID ${pid}, port ${port}`);
425 }
427 > /**
428 > * Kill a remote agent host tracked by our lockfile and remove the lockfile.
429 > */
430 export async function cleanupRemoteAgentHost(
431 exec: ISshExec,
src/vs/platform/agentHost/common/remoteAgentHostMetadata.ts 36 introduced LOC · 2 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- remoteAgentHostMetadata.ts
2 > * Copyright (c) Microsoft Corporation. All rights reserved.
3 > * Licensed under the MIT License. See License.txt in the project root for license information.
4 > *--------------------------------------------------------------------------------------------*/
5 >
6 > import { PROTOCOL_VERSION } from './state/protocol/version/registry.js';
7 >
8 > export const remoteAgentHostStateSchemaVersion = 1;
9 >
10 > /**
11 > * Persisted record describing a running `code agent host` proxy, written to
12 > * the per-quality lockfile (`~/<serverDataFolderName>/cli/agent-host-<quality>.lock`).
13 > *
14 > * This schema is shared with the Rust CLI in
15 > * `cli/src/tunnels/agent_host_metadata.rs`; field renames or removals MUST be
16 > * coordinated across both languages.
17 > */
18 > export interface IRemoteAgentHostState {
19 > readonly schemaVersion: typeof remoteAgentHostStateSchemaVersion;
20 > readonly pid: number;
21 > readonly port: number;
22 > /**
23 > * Host the supervisor's TCP listener was bound to (e.g. `127.0.0.1`,
24 > * `0.0.0.0`). Optional so older lockfiles still parse; consumers fall
25 > * back to loopback when absent.
26 > */
27 > readonly host?: string;
28 > readonly connectionToken?: string | null;
29 > readonly protocolVersion: string;
30 > readonly quality?: string;
31 > readonly tunnelName?: string;
32 > }
33 >
34 > export function createRemoteAgentHostState(options: {
35 readonly pid: number;
36 readonly port: number;
51 };
52 }
54 > export function parseRemoteAgentHostState(raw: unknown): IRemoteAgentHostState | undefined {
55 if (typeof raw !== 'object' || raw === null) {
56 return undefined;