Human-readable remote agent host address (#303758)

This commit is contained in:
Rob Lourens
2026-03-21 11:14:33 -07:00
committed by GitHub
parent 6ae7d0c592
commit 146a2ea7e7
4 changed files with 82 additions and 23 deletions

View File

@@ -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

View File

@@ -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 | `<sessionType>` | `<sessionType>:/untitled-<uuid>` | `remote-b64-xxx-copilot:/untitled-abc` |
| Existing session | `<sessionType>` | `<sessionType>:/<rawId>` | `remote-b64-xxx-copilot:/abc-123` |
| New session resource | `<sessionType>` | `<sessionType>:/untitled-<uuid>` | `remote-localhost__8081-copilot:/untitled-abc` |
| Existing session | `<sessionType>` | `<sessionType>:/<rawId>` | `remote-localhost__8081-copilot:/abc-123` |
| Backend session state | `<provider>` | `<provider>:/<rawId>` | `copilot:/abc-123` |
| Root state subscription | (string) | `agenthost:/root` | - |
| Remote filesystem | `agenthost` | `agenthost://<authority>/<path>` | `agenthost://b64-aHR0cA/home/user/project` |
| Language model ID | - | `<sessionType>:<rawModelId>` | `remote-b64-xxx-copilot:claude-sonnet-4-20250514` |
| Remote filesystem | `agenthost` | `agenthost://<authority>/<path>` | `agenthost://localhost__8081/home/user/project` |
| Language model ID | - | `<sessionType>:<rawModelId>` | `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

View File

@@ -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);
}

View File

@@ -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');