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