fix(local-agent): harden pair/reconnect auth and credential rotation

Consume pairing tokens only after successful WS registration, revoke
prior reconnect credentials on re-pair, emit reconnect.ok, and wait
for server ack before marking the extension connected.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Chengdong Zhang
2026-07-24 11:22:58 +08:00
parent 94721227c0
commit 5e743418bd

View File

@@ -30,6 +30,10 @@ export class ConnectLocalController {
resolve: (value: LocalAgentServerMessage & { type: 'pair.ok' }) => void; resolve: (value: LocalAgentServerMessage & { type: 'pair.ok' }) => void;
reject: (error: Error) => void; reject: (error: Error) => void;
}> = []; }> = [];
private reconnectWaiters: Array<{
resolve: (value: LocalAgentServerMessage & { type: 'reconnect.ok' }) => void;
reject: (error: Error) => void;
}> = [];
constructor(deps: ConnectLocalControllerDeps) { constructor(deps: ConnectLocalControllerDeps) {
this.pi = deps.pi; this.pi = deps.pi;
@@ -150,14 +154,29 @@ export class ConnectLocalController {
} }
try { try {
await this.ensureClient(stored.wsUrl || this.wsUrl); await this.ensureClient(stored.wsUrl || this.wsUrl);
this.client!.send({ const reconnectOk = await new Promise<LocalAgentServerMessage & { type: 'reconnect.ok' }>((resolve, reject) => {
type: 'reconnect', const timeout = setTimeout(() => reject(new Error('Reconnect timed out')), 30_000);
credential: stored.credential, this.reconnectWaiters.push({
projectName: stored.projectName, resolve: (value) => {
connectionEpoch: this.connectionEpoch, clearTimeout(timeout);
resolve(value);
},
reject: (error) => {
clearTimeout(timeout);
reject(error);
},
});
this.client!.send({
type: 'reconnect',
credential: stored.credential,
projectName: stored.projectName,
connectionEpoch: this.connectionEpoch,
});
}); });
this.projectName = stored.projectName; this.projectName = reconnectOk.projectName;
this.connectionEpoch = reconnectOk.connectionEpoch;
} catch { } catch {
this.projectName = undefined;
/* extension must not block Pi; reconnect can be retried manually */ /* extension must not block Pi; reconnect can be retried manually */
} }
} }
@@ -190,9 +209,19 @@ export class ConnectLocalController {
waiter?.resolve(message); waiter?.resolve(message);
return; return;
} }
if (message.type === 'reconnect.ok') {
const waiter = this.reconnectWaiters.shift();
waiter?.resolve(message);
return;
}
if (message.type === 'error') { if (message.type === 'error') {
const waiter = this.pairWaiters.shift(); const pairWaiter = this.pairWaiters.shift();
waiter?.reject(new Error(message.message)); if (pairWaiter) {
pairWaiter.reject(new Error(message.message));
return;
}
const reconnectWaiter = this.reconnectWaiters.shift();
reconnectWaiter?.reject(new Error(message.message));
return; return;
} }
if (message.type === 'task.dispatch') { if (message.type === 'task.dispatch') {