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:
@@ -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);
|
||||
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') {
|
||||
|
||||
Reference in New Issue
Block a user