agentHostFileSystemProvider.ts ×25

Frontier kind: Code frontier

unlabeled · c_e77de06bc169

852 tests · 16558 LOC · 60 files · introduces 0 tests · 253 LOC · 1 file

Introduces — evidence that enters the hierarchy at this concept

Code
25 ranges253 lines · 1 files
Tests
0 tests

Contains — complete concept membership

All code (extent)
1294 ranges16558 lines · 60 files · Browse complete extent
All tests (intent)
852 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.

1 file ranked by introduced lines: 253 introduced LOC across 25 ranges. Expand a file to inspect source; the > gutter marks introduced lines.

src/vs/platform/agentHost/common/agentHostFileSystemProvider.ts 253 introduced LOC · 25 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- agentHostFileSystemProvider.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 { decodeBase64, VSBuffer } from '../../../base/common/buffer.js';
7 > import { disposableTimeout } from '../../../base/common/async.js';
8 > import { Emitter, Event } from '../../../base/common/event.js';
9 > import { Disposable, DisposableStore, IDisposable, MutableDisposable, toDisposable } from '../../../base/common/lifecycle.js';
10 > import { URI } from '../../../base/common/uri.js';
11 > import { createFileSystemProviderError, FileChangeType, FilePermission, FileSystemProviderCapabilities, FileSystemProviderErrorCode, FileType, IFileChange, IFileDeleteOptions, IFileOverwriteOptions, IFileSystemProvider, IFileSystemProviderWithFileRealpathCapability, IFileWriteOptions, IStat, IWatchOptions } from '../../files/common/files.js';
12 > import { fromAgentHostUri, toAgentHostUri } from './agentHostUri.js';
13 > import { ContentEncoding, type CreateResourceWatchParams, type DirectoryEntry, type ResourceCopyParams, type ResourceCopyResult, type ResourceDeleteParams, type ResourceDeleteResult, type ResourceListResult, type ResourceMkdirParams, type ResourceMkdirResult, type ResourceMoveParams, type ResourceMoveResult, type ResourceReadResult, type ResourceRequestParams, type ResourceRequestResult, type ResourceResolveParams, type ResourceResolveResult, type ResourceWriteParams, type ResourceWriteResult } from './state/protocol/commands.js';
14 > import { AhpErrorCodes } from './state/protocol/errors.js';
15 > import { ProtocolError } from './state/sessionProtocol.js';
16 > import { ActionType, type ActionEnvelope } from './state/sessionActions.js';
17 > import { ROOT_STATE_URI } from './state/sessionState.js';
18 >
19 > /**
20 > * Interface for performing resource operations on a remote endpoint.
21 > *
22 > * Both {@link IAgentConnection} (client→server) and client-exposed
23 > * filesystems (server→client) satisfy this contract.
24 > */
25 > export interface IRemoteFilesystemConnection {
26 > resourceList(uri: URI): Promise<ResourceListResult>;
27 > resourceRead(uri: URI): Promise<ResourceReadResult>;
28 > resourceWrite(params: ResourceWriteParams): Promise<ResourceWriteResult>;
29 > resourceDelete(params: ResourceDeleteParams): Promise<ResourceDeleteResult>;
30 > resourceMove(params: ResourceMoveParams): Promise<ResourceMoveResult>;
31 > /** Copy a resource on the remote endpoint. */
32 > resourceCopy(params: ResourceCopyParams): Promise<ResourceCopyResult>;
33 > /**
34 > * Negotiate access to a resource the receiver mediates. Optional because
35 > * not every connection in the codebase carries one — only the agent-host
36 > * server-to-client direction needs to send `resourceRequest` today.
37 > */
38 > resourceRequest?(params: ResourceRequestParams): Promise<ResourceRequestResult>;
39 > /** Resolve (stat + realpath) a resource on the remote endpoint. */
40 > resourceResolve(params: ResourceResolveParams): Promise<ResourceResolveResult>;
41 > /** Create a directory on the remote endpoint (mkdir -p semantics). */
42 > resourceMkdir(params: ResourceMkdirParams): Promise<ResourceMkdirResult>;
43 > /**
44 > * Start a file-system watcher on the remote endpoint and return a
45 > * handle whose `onDidChange` event fires for every change the remote
46 > * reports under the watched root. Disposing the handle unsubscribes
47 > * the watch (subject to the receiver's grace window).
48 > *
49 > * Optional: implementations without subscription machinery omit it; the
50 > * filesystem provider degrades to a no-op `watch()` in that case.
51 > */
52 > watchResource?(params: CreateResourceWatchParams): Promise<IRemoteWatchHandle>;
53 > }
54 >
55 > /**
56 > * Handle for a remote file-system watcher returned by
57 > * {@link IRemoteFilesystemConnection.watchResource}. Mirrors the shape
58 > * of `IFileSystemWatcher` from `../../files/common/files.js` so the FS
59 > * provider can plug events straight into its own `onDidChangeFile`
60 > * emitter.
61 > */
62 > export interface IRemoteWatchHandle extends IDisposable {
63 > readonly onDidChange: Event<readonly IFileChange[]>;
64 > }
65 >
66 > /**
67 > * Shared implementation of {@link IAgentConnection.watchResource} —
68 > * bundles `createResourceWatch` + `subscribe` + a per-channel listener
69 > * on the action stream into an {@link IRemoteWatchHandle}. Used by
70 > * every transport that exposes those four primitives so we don't need
71 > * to duplicate the wire bookkeeping in each `IAgentConnection`
72 > * implementation.
73 > */
74 export async function createRemoteWatchHandle(
75 primitives: {
119 };
120 }
122 > /**
123 > * Build a {@link AGENT_HOST_SCHEME} URI for a given connection authority
124 > * and remote path. Assumes the remote path is a `file://` resource.
125 > */
126 > export function agentHostUri(authority: string, path: string): URI {
127 return toAgentHostUri(URI.file(path), authority);
128 }
130 > /**
131 > * Extract the remote filesystem path from a {@link AGENT_HOST_SCHEME} URI.
132 > */
133 > export function agentHostRemotePath(uri: URI): string {
134 return fromAgentHostUri(uri).path;
135 }
137 > // ---- Abstract base ----------------------------------------------------------
138 >
139 > interface IAuthorityEntry {
140 > /**
141 > * All currently-registered connections for this authority, oldest
142 > * first. The active connection is the last entry (most recent
143 > * registration wins). Older registrations are kept so that if a
144 > * caller registers `A`, then `B`, then disposes `B`, we transparently
145 > * fall back to `A` instead of entering a grace window.
146 > *
147 > * Empty while the entry is inside the grace window.
148 > */
149 > connections: IRemoteFilesystemConnection[];
150 > /**
151 > * Pending eviction timer; armed while {@link connections} is empty,
152 > * cleared on re-registration or eviction.
153 > */
154 > readonly expiry: MutableDisposable<IDisposable>;
155 > }
156 >
157 > /**
158 > * {@link IFileSystemProvider} that proxies filesystem operations
159 > * through a {@link IRemoteFilesystemConnection}.
160 > *
161 > * URIs encode the original scheme and authority in the path so any remote
162 > * resource can be represented. Subclasses provide the URI decode function
163 > * and scheme-specific helpers.
164 > *
165 > * Individual connections are identified by the URI's authority component.
166 > */
167 > export abstract class AHPFileSystemProvider extends Disposable implements IFileSystemProvider, IFileSystemProviderWithFileRealpathCapability {
168 >
169 > readonly capabilities =
170 > FileSystemProviderCapabilities.PathCaseSensitive |
171 > FileSystemProviderCapabilities.FileReadWrite |
172 > FileSystemProviderCapabilities.FileFolderCopy |
173 > FileSystemProviderCapabilities.FileRealpath;
174 >
175 > private readonly _onDidChangeCapabilities = this._register(new Emitter<void>());
176 > readonly onDidChangeCapabilities = this._onDidChangeCapabilities.event;
177 >
178 > private readonly _onDidChangeFile = this._register(new Emitter<readonly IFileChange[]>());
179 > readonly onDidChangeFile = this._onDidChangeFile.event;
180 > private readonly _onDidWatchError = this._register(new Emitter<string>());
181 > readonly onDidWatchError = this._onDidWatchError.event;
182 >
183 > /**
184 > * Per-authority registration slot. We keep the slot alive for a brief
185 > * grace period after the last registration is disposed, so an
186 > * operation issued during a reconnection window can wait for the
187 > * replacement registration instead of failing immediately.
188 > */
189 > private readonly _authorities = new Map<string, IAuthorityEntry>();
190 >
191 > /**
192 > * Fires the authority whose active connection has changed: added,
193 > * replaced, fallen back to an older registration, entered the grace
194 > * window (no active connection), or evicted. Long-lived consumers
195 > * (e.g. {@link watch}) subscribe here so they continue to receive
196 > * notifications across full entry eviction + later re-creation —
197 > * something a per-entry emitter cannot offer.
198 > */
199 > private readonly _onDidChangeConnection = this._register(new Emitter<string>());
200 >
201 > /**
202 > * Grace period during which {@link _getConnection} will await a new
203 > * registration after the previous one is disposed. Covers the window
204 > * where a transport is briefly torn down and re-registered (e.g. an
205 > * agent-host client reconnect that races a plugin sync). 5s matches
206 > * the typical reconnect timeout. Consumers should still implement
207 > * logical retries for longer reconnection latencies, but this is a
208 > * low level, best-effort mechanism.
209 > *
210 > * Tests can override this via the constructor parameter.
211 > */
212 > private static readonly _DEFAULT_CONNECTION_GRACE_MS = 5000;
213 >
214 > constructor(
215 private readonly _connectionGraceMs: number = AHPFileSystemProvider._DEFAULT_CONNECTION_GRACE_MS,
216 ) {
217 super();
218 }
220 > /**
221 > * Register a mapping from a URI authority to a connection.
222 > * Returns a disposable that unregisters the mapping. Multiple
223 > * concurrent registrations for the same authority are supported;
224 > * the most recent registration wins, and disposing it falls back to
225 > * the previous one (if any). After the *last* registration is
226 > * disposed the entry is held open for {@link _connectionGraceMs} so
227 > * that a reconnect can replace it without orphaning in-flight
228 > * operations.
229 > */
230 > registerAuthority(authority: string, connection: IRemoteFilesystemConnection): IDisposable {
231 let entry = this._authorities.get(authority);
232 if (!entry) {
263 });
264 }
266 > private _expireAuthority(authority: string, entry: IAuthorityEntry): void {
267 // A re-registration may have landed between scheduling and
268 // firing — bail in that case.
274 this._onDidChangeConnection.fire(authority);
275 }
277 > override dispose(): void {
278 for (const entry of this._authorities.values()) {
279 entry.expiry.dispose();
283 super.dispose();
284 }
286 > /** Decode a provider URI back to the original URI for the remote endpoint. */
287 > protected abstract _decodeUri(resource: URI): URI;
288 >
289 > /** Encode a remote URI back into a provider URI with the given authority. */
290 > protected abstract _encodeUri(resource: URI, authority: string): URI;
291 >
292 > watch(resource: URI, opts: IWatchOptions): IDisposable {
293 // `IFileSystemProvider.watch` is synchronous, but acquiring a
294 // connection may have to wait for a (re)registration and the
416 }
417 }
419 > async realpath(resource: URI): Promise<string> {
420 const path = resource.path;
421 // Synthetic roots and virtual content schemes have no distinct
439 }
440 }
442 > async readdir(resource: URI): Promise<[string, FileType][]> {
443 const entries = await this._listDirectory(resource.authority, resource);
444 return entries.map(e => [e.name, e.type === 'directory' ? FileType.Directory : FileType.File]);
445 }
447 > async readFile(resource: URI): Promise<Uint8Array> {
448 const connection = await this._getConnection(resource.authority);
449 try {
458 }
459 }
461 > async writeFile(resource: URI, content: Uint8Array, _opts: IFileWriteOptions): Promise<void> {
462 const connection = await this._getConnection(resource.authority);
463 try {
473 }
474 }
476 > async mkdir(resource: URI): Promise<void> {
477 const connection = await this._getConnection(resource.authority);
478 try {
483 }
484 }
486 > async delete(resource: URI, opts: IFileDeleteOptions): Promise<void> {
487 const connection = await this._getConnection(resource.authority);
488 try {
493 }
494 }
496 > async rename(from: URI, to: URI, opts: IFileOverwriteOptions): Promise<void> {
497 const connection = await this._getConnection(from.authority);
498 try {
504 }
505 }
507 > async copy(from: URI, to: URI, opts: IFileOverwriteOptions): Promise<void> {
508 const connection = await this._getConnection(from.authority);
509 try {
515 }
516 }
518 > /**
519 > * Negotiate access to {@link resource} with the receiver, asking for the
520 > * granted modes in {@link opts}. Used after a `NoPermissions` failure to
521 > * prompt the receiver to grant access; the caller can then retry.
522 > *
523 > * Resolves on success. Rejects if the receiver denies, the connection
524 > * is missing, or the connection doesn't implement `resourceRequest`.
525 > */
526 > async requestResourceAccess(resource: URI, opts: { readonly read?: boolean; readonly write?: boolean }): Promise<void> {
527 const connection = await this._getConnection(resource.authority);
528 if (!connection.resourceRequest) {
544 }
545 }
547 > // ---- Internals ----------------------------------------------------------
548 >
549 > private _getConnection(authority: string): Promise<IRemoteFilesystemConnection> {
550 const entry = this._authorities.get(authority);
551 if (!entry) {
591 });
592 }
594 > /**
595 > * Translate a thrown error from a {@link IRemoteFilesystemConnection}
596 > * into a {@link FileSystemProviderError}. Preserves `PermissionDenied`
597 > * (-32009) as `NoPermissions` so callers can distinguish a
598 > * permission failure from `NotFound` and decide whether to negotiate
599 > * via {@link requestResourceAccess}.
600 > */
601 > private _mapError(err: unknown, defaultCode: FileSystemProviderErrorCode): Error {
602 if (err instanceof ProtocolError && err.code === AhpErrorCodes.PermissionDenied) {
603 return createFileSystemProviderError(err.message, FileSystemProviderErrorCode.NoPermissions);
608 );
609 }
611 > /**
612 > * Resolve a decoded resource over {@link connection}. Shared by
613 > * {@link stat} and {@link realpath}.
614 > */
615 > private _resolve(connection: IRemoteFilesystemConnection, decoded: URI): Promise<ResourceResolveResult> {
616 return connection.resourceResolve({ channel: ROOT_STATE_URI, uri: decoded.toString() });
617 }
619 > private async _listDirectory(authority: string, resource: URI): Promise<readonly DirectoryEntry[]> {
620 const connection = await this._getConnection(authority);
621 try {
627 }
628 }
630 >
631 > // ---- Agent Host filesystem (client reads agent host files) ------------------
632 >
633 > /**
634 > * Filesystem provider for accessing agent host files from the
635 > * client side. Registered under the `vscode-agent-host` scheme.
636 > *
637 > * ```
638 > * vscode-agent-host://[connectionAuthority]/[originalScheme]/[originalAuthority]/[originalPath]
639 > * ```
640 > */
641 > export class AgentHostFileSystemProvider extends AHPFileSystemProvider {
642 > protected _decodeUri(resource: URI): URI {
643 return fromAgentHostUri(resource);
644 }
646 > protected _encodeUri(resource: URI, authority: string): URI {
647 return toAgentHostUri(resource, authority);
648 }