diff --git a/.github/instructions/remoteAgentHost.instructions.md b/.github/instructions/remoteAgentHost.instructions.md new file mode 100644 index 00000000000..aa3f24b290e --- /dev/null +++ b/.github/instructions/remoteAgentHost.instructions.md @@ -0,0 +1,35 @@ +--- +description: Architecture documentation for remote agent host connections. Use when working in `src/vs/sessions/contrib/remoteAgentHost` +applyTo: src/vs/sessions/contrib/remoteAgentHost/** +--- + +# Remote Agent Host + +The remote agent host feature connects the sessions app to agent host processes running on other machines over WebSocket. + +## Key Files + +- `ARCHITECTURE.md` - full architecture documentation (URI conventions, registration flow, data flow diagram) +- `REMOTE_AGENT_HOST_RECONNECTION.md` - reconnection lifecycle spec (15 numbered requirements) +- `browser/remoteAgentHost.contribution.ts` - central orchestrator +- `browser/agentHostFileSystemProvider.ts` - read-only FS provider for remote browsing + +## Architecture Documentation + +When making changes to this feature area, **review and update `ARCHITECTURE.md`** if your changes affect: + +- Connection lifecycle (connect, disconnect, reconnect) +- Agent registration flow +- URI conventions or naming +- Session creation flow +- The data flow diagram + +The doc lives at `src/vs/sessions/contrib/remoteAgentHost/ARCHITECTURE.md`. + +## Related Code Outside This Folder + +- `src/vs/platform/agentHost/common/remoteAgentHostService.ts` - service interface (`IRemoteAgentHostService`) +- `src/vs/platform/agentHost/electron-browser/remoteAgentHostServiceImpl.ts` - Electron implementation +- `src/vs/platform/agentHost/electron-browser/remoteAgentHostProtocolClient.ts` - WebSocket protocol client +- `src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostSessionListController.ts` - session list sidebar +- `src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostSessionHandler.ts` - session content provider diff --git a/src/vs/sessions/contrib/remoteAgentHost/ARCHITECTURE.md b/src/vs/sessions/contrib/remoteAgentHost/ARCHITECTURE.md index aae3798a801..b45c0aaf863 100644 --- a/src/vs/sessions/contrib/remoteAgentHost/ARCHITECTURE.md +++ b/src/vs/sessions/contrib/remoteAgentHost/ARCHITECTURE.md @@ -98,9 +98,13 @@ Remote addresses are encoded into URI-safe authority strings via `agentHostAuthority(address)`: - Alphanumeric addresses pass through unchanged -- Others are url-safe base64 encoded with a `b64-` prefix +- "Normal" addresses (`[a-zA-Z0-9.:-]`) get colons replaced with `__` +- Everything else is url-safe base64 encoded with a `b64-` prefix -Example: `http://127.0.0.1:3000` → `b64-aHR0cDovLzEyNy4wLjAuMTozMDAw` +Examples: +- `localhost:8081` → `localhost__8081` +- `192.168.1.1:8080` → `192.168.1.1__8080` +- `http://127.0.0.1:3000` → `b64-aHR0cDovLzEyNy4wLjAuMTozMDAw` ## Agent Registration @@ -110,10 +114,10 @@ When `_registerAgent()` is called for a discovered copilot agent from address `X | Concept | Value | Example | |---------|-------|---------| -| **Authority** | `agentHostAuthority(address)` | `b64-aHR0cA` | -| **Session type** | `remote-${authority}-${provider}` | `remote-b64-aHR0cA-copilot` | -| **Agent ID** | same as session type | `remote-b64-aHR0cA-copilot` | -| **Vendor** | same as session type | `remote-b64-aHR0cA-copilot` | +| **Authority** | `agentHostAuthority(address)` | `localhost__8081` | +| **Session type** | `remote-${authority}-${provider}` | `remote-localhost__8081-copilot` | +| **Agent ID** | same as session type | `remote-localhost__8081-copilot` | +| **Vendor** | same as session type | `remote-localhost__8081-copilot` | | **Display name** | `configuredName \|\| "${displayName} (${address})"` | `dev-box` | ### Four Registrations Per Agent @@ -134,23 +138,23 @@ When `_registerAgent()` is called for a discovered copilot agent from address `X 4. **Language model provider** - `AgentHostLanguageModelProvider` registers models under the vendor descriptor. Model IDs are prefixed with the session - type (e.g., `remote-b64-xxx-copilot:claude-sonnet-4-20250514`). + type (e.g., `remote-localhost__8081-copilot:claude-sonnet-4-20250514`). ## URI Conventions | Context | Scheme | Format | Example | |---------|--------|--------|---------| -| New session resource | `` | `:/untitled-` | `remote-b64-xxx-copilot:/untitled-abc` | -| Existing session | `` | `:/` | `remote-b64-xxx-copilot:/abc-123` | +| New session resource | `` | `:/untitled-` | `remote-localhost__8081-copilot:/untitled-abc` | +| Existing session | `` | `:/` | `remote-localhost__8081-copilot:/abc-123` | | Backend session state | `` | `:/` | `copilot:/abc-123` | | Root state subscription | (string) | `agenthost:/root` | - | -| Remote filesystem | `agenthost` | `agenthost:///` | `agenthost://b64-aHR0cA/home/user/project` | -| Language model ID | - | `:` | `remote-b64-xxx-copilot:claude-sonnet-4-20250514` | +| Remote filesystem | `agenthost` | `agenthost:///` | `agenthost://localhost__8081/home/user/project` | +| Language model ID | - | `:` | `remote-localhost__8081-copilot:claude-sonnet-4-20250514` | ### Key distinction: session resource vs backend session URI - The **session resource** URI uses the session type as its scheme - (e.g., `remote-b64-xxx-copilot:/untitled-abc`). This is the URI visible to + (e.g., `remote-localhost__8081-copilot:/untitled-abc`). This is the URI visible to the chat UI and session management. - The **backend session** URI uses the provider as its scheme (e.g., `copilot:/abc-123`). This is sent over the agent host protocol to the @@ -173,8 +177,8 @@ remote host, then picks a folder on the remote filesystem. This produces a `SessionWorkspace` with an `agenthost://` URI: ``` -agenthost://b64-aHR0cA/home/user/myproject - ↑ authority ↑ remote filesystem path +agenthost://localhost__8081/home/user/myproject + ↑ authority ↑ remote filesystem path ``` ### 2. Session Target Resolution @@ -184,7 +188,7 @@ resolves the matching session type via `getRemoteAgentHostSessionTarget()` (defined in `remoteAgentHost.contribution.ts`): ```typescript -// authority "b64-aHR0cA" → find connection → "remote-b64-aHR0cA-copilot" +// authority "localhost__8081" → find connection → "remote-localhost__8081-copilot" const target = getRemoteAgentHostSessionTarget(connections, authority); ``` @@ -194,7 +198,7 @@ const target = getRemoteAgentHostSessionTarget(connections, authority); ```typescript URI.from({ scheme: target, path: `/untitled-${generateUuid()}` }) -// → remote-b64-aHR0cA-copilot:/untitled-abc-123 +// → remote-localhost__8081-copilot:/untitled-abc-123 ``` ### 4. Session Object Creation diff --git a/src/vs/sessions/contrib/remoteAgentHost/browser/remoteAgentHost.contribution.ts b/src/vs/sessions/contrib/remoteAgentHost/browser/remoteAgentHost.contribution.ts index 4971236cb5a..545dbb83d16 100644 --- a/src/vs/sessions/contrib/remoteAgentHost/browser/remoteAgentHost.contribution.ts +++ b/src/vs/sessions/contrib/remoteAgentHost/browser/remoteAgentHost.contribution.ts @@ -34,14 +34,21 @@ import { Registry } from '../../../../platform/registry/common/platform.js'; * Encode a remote address into an identifier that is safe for use in * both URI schemes and URI authorities, and is collision-free. * - * If the address contains only alphanumeric characters it is returned as-is. - * Otherwise it is url-safe base64-encoded (no padding) to guarantee the - * result contains only `[A-Za-z0-9_-]`. + * Three tiers: + * 1. Purely alphanumeric addresses are returned as-is. + * 2. "Normal" addresses containing only `[a-zA-Z0-9.:-]` get colons + * replaced with `__` (double underscore) for human readability. + * Addresses containing `_` skip this tier to keep the encoding + * collision-free (`__` can only appear from colon replacement). + * 3. Everything else is url-safe base64-encoded with a `b64-` prefix. */ export function agentHostAuthority(address: string): string { if (/^[a-zA-Z0-9]+$/.test(address)) { return address; } + if (/^[a-zA-Z0-9.:\-]+$/.test(address)) { + return address.replaceAll(':', '__'); + } return 'b64-' + encodeBase64(VSBuffer.fromString(address), false, true); } diff --git a/src/vs/sessions/contrib/remoteAgentHost/test/browser/agentHostFileSystemProvider.test.ts b/src/vs/sessions/contrib/remoteAgentHost/test/browser/agentHostFileSystemProvider.test.ts index e22ba6e32fc..6761d7455c3 100644 --- a/src/vs/sessions/contrib/remoteAgentHost/test/browser/agentHostFileSystemProvider.test.ts +++ b/src/vs/sessions/contrib/remoteAgentHost/test/browser/agentHostFileSystemProvider.test.ts @@ -52,13 +52,26 @@ suite('AgentHostAuthority - encoding', () => { assert.strictEqual(agentHostAuthority('localhost'), 'localhost'); }); - test('address with special characters is base64-encoded', () => { - const authority = agentHostAuthority('localhost:8081'); - assert.ok(authority.startsWith('b64-')); + test('normal host:port address uses human-readable encoding', () => { + assert.strictEqual(agentHostAuthority('localhost:8081'), 'localhost__8081'); + assert.strictEqual(agentHostAuthority('192.168.1.1:8080'), '192.168.1.1__8080'); + assert.strictEqual(agentHostAuthority('my-host:9090'), 'my-host__9090'); + assert.strictEqual(agentHostAuthority('host.name:80'), 'host.name__80'); + }); + + test('address with underscore falls through to base64', () => { + const authority = agentHostAuthority('host_name:8080'); + assert.ok(authority.startsWith('b64-'), `expected base64 for underscore address, got: ${authority}`); + }); + + test('address with exotic characters is base64-encoded', () => { + assert.ok(agentHostAuthority('user@host:8080').startsWith('b64-')); + assert.ok(agentHostAuthority('host with spaces').startsWith('b64-')); + assert.ok(agentHostAuthority('http://myhost:3000').startsWith('b64-')); }); test('different addresses produce different authorities', () => { - const cases = ['localhost:8080', 'localhost:8081', '192.168.1.1:8080', 'host-name:80', 'host.name:80']; + const cases = ['localhost:8080', 'localhost:8081', '192.168.1.1:8080', 'host-name:80', 'host.name:80', 'host_name:80', 'user@host:8080']; const results = cases.map(agentHostAuthority); const unique = new Set(results); assert.strictEqual(unique.size, cases.length, 'all authorities must be unique');