Files
vscode/src/vs/platform/agentHost/node/agentHostUpgradeChannel.ts
T
Connor Peet 56fe549585 agent host: lenient protocol negotiation + CLI-driven server upgrades
Replaces the exact-match version check in `ProtocolServerHandler._handleInitialize`
with a semver-caret negotiator (`negotiateProtocolVersion`) that picks the
highest offered version compatible with the server.

When negotiation still fails and the agent host was spawned by a managing
VS Code CLI (signalled by the new `VSCODE_AGENT_HOST_MANAGEMENT_SOCKET`
env var), the `UnsupportedProtocolVersion` error advertises a
`_meta.vscodeUpgradeMethod = "_vscodeUpgrade"` hint. The client can then
invoke that method on the same transport (callable pre-`initialize`) to
request a server upgrade.

The CLI runs a hyper-based HTTP control server on a unix socket / named
pipe. `POST /upgrade` synchronously downloads the latest release, returns
a serde-derived response (`{ok, upgradeNeeded, upgradeStarted,
runningCommit, latestCommit, restartDelayMs, error}`), and then schedules
a kill+respawn after a 3 s drain delay so the response can hop back
through the proxy before the transport drops. Single-flight via
`upgrade_in_progress: AtomicBool`; the listener is started lazily from
`AgentHostManager::start_server`.

UI:
- `RemoteAgentHostConnectionStatus.incompatible` carries `vscodeUpgradeMethod`
  (read from `_meta`).
- A `watchForIncompatibleNotifications` autorun on each provider raises a
  one-shot warning notification on transition into `incompatible`, with
  "Update Server" (when the host advertised it) and "Show Options"
  primary actions.
- `runServerUpgrade` is shared between the notification action and the
  per-host quickpick. It drives a progress notification with a per-second
  "Restarting in Ns..." countdown and observes `connectionStatus` so it
  bails out if some other code path is already reconnecting.
2026-05-15 21:34:22 -07:00

90 lines
3.6 KiB
TypeScript

/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/**
* Environment variable populated by the VS Code CLI when it spawns the
* agent host server. Its value is the path of a unix-domain socket
* (POSIX) or named pipe (Windows) on which the CLI is serving its HTTP
* management API. Presence of the variable also serves as the "I was
* spawned by a managing CLI" marker that decides whether the server
* advertises an in-band upgrade method to clients.
*/
export const VSCODE_AGENT_HOST_MANAGEMENT_SOCKET_ENV = 'VSCODE_AGENT_HOST_MANAGEMENT_SOCKET';
/**
* Status payload returned by the CLI's `POST /upgrade` endpoint. Sent
* back verbatim to the agent host client so the UI can surface it.
*/
export interface IUpgradeRequestResponse {
readonly ok: boolean;
/** Whether the running server is older than the latest known release. */
readonly upgradeNeeded?: boolean;
/** Whether the CLI committed to performing the upgrade (true => kill+respawn was scheduled). */
readonly upgradeStarted?: boolean;
/** Commit hash of the currently running server, or `null` if none. */
readonly runningCommit?: string | null;
/** Commit hash of the latest known release at the time of the call. */
readonly latestCommit?: string;
/** Milliseconds the client should wait before reconnecting (only set when upgrade started). */
readonly restartDelayMs?: number;
/** Human-readable error message when `ok` is false. */
readonly error?: string;
}
/**
* Returns the management socket path advertised by the hosting CLI, or
* `undefined` when the current process was not spawned by one.
*/
export function getAgentHostManagementSocketPath(): string | undefined {
const value = process.env[VSCODE_AGENT_HOST_MANAGEMENT_SOCKET_ENV];
return value && value.length > 0 ? value : undefined;
}
/**
* Ask the hosting CLI to check for an update and (if needed) restart this
* server. Sends `POST /upgrade` to the management socket and returns the
* CLI's parsed JSON response.
*
* Rejects when no management socket is advertised, when the connection
* fails, on non-2xx responses, or when the response body cannot be parsed.
*/
export async function requestAgentHostUpgrade(socketPath = getAgentHostManagementSocketPath()): Promise<IUpgradeRequestResponse> {
const http = await import('http');
if (!socketPath) {
return Promise.reject(new Error(`Cannot request upgrade: ${VSCODE_AGENT_HOST_MANAGEMENT_SOCKET_ENV} is not set.`));
}
return new Promise<IUpgradeRequestResponse>((resolve, reject) => {
const req = http.request({
socketPath,
method: 'POST',
path: '/upgrade',
headers: { 'content-length': '0' },
}, (res) => {
const chunks: Buffer[] = [];
res.on('data', (chunk: Buffer) => chunks.push(chunk));
res.on('end', () => {
const body = Buffer.concat(chunks).toString('utf8');
const status = res.statusCode ?? 0;
let parsed: IUpgradeRequestResponse | undefined;
try {
parsed = body ? JSON.parse(body) as IUpgradeRequestResponse : undefined;
} catch {
// fall through to error reporting below
}
if (status >= 200 && status < 300 && parsed && parsed.ok !== false) {
resolve(parsed);
} else {
const reason = parsed?.error || body || `HTTP ${status}`;
reject(new Error(`Agent host upgrade request failed: ${reason}`));
}
});
res.on('error', reject);
});
req.once('error', reject);
req.end();
});
}