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