From 312b30e5fe861cc6a514b0e63363c353042ab506 Mon Sep 17 00:00:00 2001 From: NagyVikt Date: Sun, 12 Apr 2026 02:43:04 +0200 Subject: [PATCH] Prevent self-update prompt hangs and require explicit confirmation The interactive self-update path now uses strict y/n parsing and adds short backoff handling for transient non-blocking stdin read errors so status no longer spins on idle pipes. Tests were updated to assert the strict prompt contract. Constraint: Existing status command must remain interactive-friendly and non-destructive by default Rejected: Keep permissive Enter default for update prompt | conflicts with explicit confirmation policy Confidence: high Scope-risk: narrow Reversibility: clean Directive: Keep self-update prompt semantics synchronized between CLI logic and regex assertions Tested: npm test --- bin/multiagent-safety.js | 19 ++++++++++++++++--- test/install.test.js | 4 ++-- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/bin/multiagent-safety.js b/bin/multiagent-safety.js index fc98160..a6e4b7b 100755 --- a/bin/multiagent-safety.js +++ b/bin/multiagent-safety.js @@ -1769,6 +1769,12 @@ function isInteractiveTerminal() { return Boolean(process.stdin.isTTY && process.stdout.isTTY); } +const stdinWaitArray = new Int32Array(new SharedArrayBuffer(4)); + +function sleepSyncMs(milliseconds) { + Atomics.wait(stdinWaitArray, 0, 0, milliseconds); +} + function readSingleLineFromStdin() { let input = ''; const buffer = Buffer.alloc(1); @@ -1777,11 +1783,19 @@ function readSingleLineFromStdin() { let bytesRead = 0; try { bytesRead = fs.readSync(process.stdin.fd, buffer, 0, 1); - } catch { + } catch (error) { + if (error && ['EAGAIN', 'EWOULDBLOCK', 'EINTR'].includes(error.code)) { + sleepSyncMs(15); + continue; + } return input; } if (bytesRead === 0) { + if (process.stdin.isTTY) { + sleepSyncMs(15); + continue; + } return input; } @@ -1921,9 +1935,8 @@ function maybeSelfUpdateBeforeStatus() { } const shouldUpdate = interactive - ? promptYesNo( + ? promptYesNoStrict( `Update now? (${NPM_BIN} i -g ${packageJson.name}@latest)`, - false, ) : autoApproval; diff --git a/test/install.test.js b/test/install.test.js index 018ccfa..d630a51 100644 --- a/test/install.test.js +++ b/test/install.test.js @@ -763,11 +763,11 @@ exit 1 assert.equal(fs.existsSync(markerPath), true, 'expected self-update command to run'); }); -test('self-update prompt defaults to no when approval is not preconfigured', () => { +test('self-update prompt requires explicit y/n when approval is not preconfigured', () => { const source = fs.readFileSync(cliPath, 'utf8'); assert.match( source, - /const shouldUpdate = interactive\s*\?\s*promptYesNo\(\s*`Update now\?\s*\(\$\{NPM_BIN\} i -g \$\{packageJson\.name\}@latest\)`\s*,\s*false,\s*\)\s*:\s*autoApproval;/s, + /const shouldUpdate = interactive\s*\?\s*promptYesNoStrict\(\s*`Update now\?\s*\(\$\{NPM_BIN\} i -g \$\{packageJson\.name\}@latest\)`\s*,?\s*\)\s*:\s*autoApproval;/s, ); });