From 5e743418bdfdae942804c5b63653cc86fdb6d910 Mon Sep 17 00:00:00 2001 From: Chengdong Zhang Date: Fri, 24 Jul 2026 11:22:58 +0800 Subject: [PATCH] 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 --- src/connectLocalController.ts | 45 +++++++++++++++++++++++++++++++++++-------- 1 file changed, 37 insertions(+), 8 deletions(-) diff --git a/src/connectLocalController.ts b/src/connectLocalController.ts index 77931e1..158c21d 100644 --- a/src/connectLocalController.ts +++ b/src/connectLocalController.ts @@ -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((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') {