Skip to content

feat: sudo password prompt via chat UI (askpass bridge)#845

Closed
szmidtpiotr wants to merge 2 commits into
siteboon:mainfrom
szmidtpiotr:pr/sudo-askpass
Closed

feat: sudo password prompt via chat UI (askpass bridge)#845
szmidtpiotr wants to merge 2 commits into
siteboon:mainfrom
szmidtpiotr:pr/sudo-askpass

Conversation

@szmidtpiotr

@szmidtpiotr szmidtpiotr commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Small chunk from #801 (splitting #816 per maintainer request).

What

Agent/CLI sudo commands that need a password now prompt the user through the chat UI instead of hanging. A per-run token registers a loopback-only askpass bridge; the password is requested over the chat WebSocket and handed back to the waiting sudo process via SUDO_ASKPASS.

  • server/modules/sudo-askpass/ — per-run token registry + loopback askpass HTTP route
  • providers (claude-sdk, cursor, gemini, opencode) register/unregister a sudo run and inject SUDO_ASKPASS around each invocation
  • chat WebSocket handles sudo-password-response; new sudo_password_request / sudo_password_cancelled message kinds
  • frontend: SudoPasswordModal + useSudoPasswordPrompt hook; Shell sudo-prompt detection util
  • unit tests for askpass integration, service, sudo-prompt, and the shell util

Verification

npm run typecheck passes (client + server) against current main (v1.33.2).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Detects sudo password prompts and shows a focused password-entry modal with submit/cancel.
    • Enables agents and subprocesses to request sudo passwords via the chat UI so prompts surface in-app.
    • Adds localization strings for sudo prompts.
  • Bug Fixes

    • Improved reliability and cleanup of sudo prompt sessions to avoid stale prompt state.

Agent/CLI sudo commands that need a password now prompt the user through
the chat UI instead of hanging. A per-run token registers a loopback-only
askpass bridge; the password is requested over the chat WebSocket and
resolved back to the waiting sudo process.

- sudo-askpass module: per-run token registry + loopback askpass HTTP route
- providers (claude-sdk, cursor, gemini, opencode) register/unregister a
  sudo run and inject SUDO_ASKPASS env around each invocation
- chat WebSocket handles sudo-password-response; new sudo_password_request /
  sudo_password_cancelled message kinds
- frontend: SudoPasswordModal + useSudoPasswordPrompt hook, Shell sudo util
- tests for askpass integration, service, sudo-prompt, and shell sudo util

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 72a31d9d-d74d-43b3-b986-f7171eaaca7f

📥 Commits

Reviewing files that changed from the base of the PR and between afa328d and 2e2ab9d.

📒 Files selected for processing (1)
  • server/opencode-cli.js
🚧 Files skipped from review as they are similar to previous changes (1)
  • server/opencode-cli.js

📝 Walkthrough

Walkthrough

Adds a sudo askpass bridge: server-side service that registers per-run tokens and env, an internal loopback route for askpass helpers, WebSocket message extensions for prompting and resolving passwords, subprocess environment wiring for multiple CLIs, frontend chat and shell modals to collect passwords, and tests.

Changes

Sudo password handling system

Layer / File(s) Summary
Core sudo-askpass service and tests
server/modules/sudo-askpass/sudo-askpass.service.js, server/modules/sudo-askpass/sudo-askpass.service.test.ts
Service implements run registration, high-entropy tokens, embedded askpass helper and shim, request/resolve lifecycle with timeouts, WebSocket relay, token validation, and unit tests covering APIs and edge cases.
Server wiring and internal askpass route
server/index.js, server/routes/internal-askpass.js, server/modules/websocket/services/chat-websocket.service.ts, server/shared/types.ts, server/modules/websocket/utils/sudo-prompt.ts
Mounts loopback POST /internal/askpass, passes resolveSudoPassword into chat WebSocket handler, sets service port, extends MessageKind with sudo events, and adds websocket message routing/tests for sudo-password-response.
CLI subprocess sudo environment integration
server/claude-sdk.js, server/cursor-cli.js, server/gemini-cli.js, server/opencode-cli.js
Registers sudo run contexts when spawning subprocesses, merges sudoRun.env into spawned environments, and ensures unregisterSudoRun is called on process close/error.
Chat interface sudo password collection
src/components/chat/hooks/useSudoPasswordPrompt.ts, src/components/chat/view/ChatInterface.tsx, src/components/chat/view/subcomponents/SudoPasswordModal.tsx, src/i18n/locales/en/chat.json, src/stores/useSessionStore.ts
Adds hook to detect sudo_password_request/sudo_password_cancelled messages, modal UI to collect password with submit/cancel, wiring to send sudo-password-response, and i18n strings.
Terminal shell sudo detection and UI
src/components/shell/utils/sudo.ts, src/components/shell/utils/sudo.test.ts, src/components/shell/view/Shell.tsx, server/modules/websocket/utils/sudo-prompt.test.ts
Adds prompt detector using a [sudo] marker regex, tests for detection and false positives, Shell overlay modal for password input, submit/cancel handling, and clearing on disconnect.
Askpass integration tests
server/modules/sudo-askpass/askpass-integration.test.ts
Integration tests spawn the askpass helper against an ephemeral server and validate successful password retrieval and failure for unknown tokens.

Suggested reviewers

  • viper151
  • blackmammoth

"🐰 I watch the prompt, soft in my paw,
I hop to the modal and open the drawer,
From chat or shell the password I bring,
A tiny brim of safety — now let sudo sing."

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 42.31% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and specifically describes the main change: implementing sudo password prompts that appear in the chat UI instead of hanging, using an askpass bridge architecture.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
server/opencode-cli.js (1)

205-212: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

spawnEnv is undefined – runtime error and sudo env not injected.

Object.assign(spawnEnv, sudoRun.env) references spawnEnv, which is never declared. This will throw a ReferenceError at runtime. Additionally, even if it didn't error, the spawn call at line 211 uses { ...process.env } instead of the merged environment, so the sudo askpass variables would never reach the subprocess.

🐛 Proposed fix
     void providerModelsService.resolveResumeModel('opencode', sessionId, model).then((resolvedModel) => {
       const args = ['run', '--format', 'json'];
       if (sessionId) {
         args.push('--session', sessionId);
       }
       if (resolvedModel) {
         args.push('--model', resolvedModel);
       }
       if (command && command.trim()) {
         args.push(command.trim());
       }

       const sudoRun = registerSudoRun(ws, sessionId, 'opencode');
-      Object.assign(spawnEnv, sudoRun.env);
+      const spawnEnv = { ...process.env, ...sudoRun.env };

       opencodeProcess = spawnFunction('opencode', args, {
         cwd: workingDir,
         stdio: ['pipe', 'pipe', 'pipe'],
-        env: { ...process.env },
+        env: spawnEnv,
       });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/opencode-cli.js` around lines 205 - 212, The code references an
undeclared spawnEnv and then ignores the merged env when calling spawnFunction;
declare and populate a spawnEnv object from process.env, merge in the
sudoRun.env returned by registerSudoRun (sudoRun and registerSudoRun are the
relevant symbols) using Object.assign or spread, and pass that merged spawnEnv
as the env option to spawnFunction when creating opencodeProcess so the sudo
askpass variables are injected and no ReferenceError occurs.
🧹 Nitpick comments (2)
server/modules/sudo-askpass/askpass-integration.test.ts (1)

53-77: ⚡ Quick win

Move teardown into finally to prevent resource leakage on failures.

Line 75-76 and Line 93 only run on success paths. If an assertion fails earlier, the HTTP server and sudo-run registration can leak and make later tests flaky.

Suggested fix
 test('askpass helper fetches the typed password over the loopback route', async () => {
   const { server, port } = await startServer();
   setServerPort(port);

   const sent: string[] = [];
   const ws = { send: (data: string) => sent.push(data) };
-  const { token } = registerSudoRun(ws, 'sess-int', 'claude');
-  const { askpassPath } = ensureAskpassFiles();
-
-  const helper = runHelper(askpassPath, '[sudo] password for piotr: ', {
-    ...process.env,
-    AIGM_ASKPASS_TOKEN: token,
-    AIGM_ASKPASS_PORT: String(port),
-  });
-
-  await waitFor(() => sent.length > 0);
-  const requestId = JSON.parse(sent[0]).requestId;
-  resolveSudoPassword(requestId, { password: 'hunter2' });
-
-  assert.equal(await helper.code(), 0);
-  assert.equal(helper.stdout(), 'hunter2');
-
-  unregisterSudoRun(token);
-  await new Promise((r) => server.close(r));
+  const { token } = registerSudoRun(ws, 'sess-int', 'claude');
+  try {
+    const { askpassPath } = ensureAskpassFiles();
+    const helper = runHelper(askpassPath, '[sudo] password for piotr: ', {
+      ...process.env,
+      AIGM_ASKPASS_TOKEN: token,
+      AIGM_ASKPASS_PORT: String(port),
+    });
+
+    await waitFor(() => sent.length > 0);
+    const requestId = JSON.parse(sent[0]).requestId;
+    resolveSudoPassword(requestId, { password: 'hunter2' });
+
+    assert.equal(await helper.code(), 0);
+    assert.equal(helper.stdout(), 'hunter2');
+  } finally {
+    unregisterSudoRun(token);
+    await new Promise((r) => server.close(r));
+  }
 });

Also applies to: 79-94

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/modules/sudo-askpass/askpass-integration.test.ts` around lines 53 -
77, The test currently registers a sudo run and starts an HTTP server but only
calls unregisterSudoRun(token) and server.close() on the success path; wrap the
main test logic (everything after startServer()/registerSudoRun() and before
teardown) in a try/finally block so that unregisterSudoRun(token) and awaiting
server.close() always run in the finally clause, ensuring resources started by
startServer(), registerSudoRun(), and ensureAskpassFiles()/runHelper() are
cleaned up even on assertion failures; reference the existing symbols
startServer, setServerPort, registerSudoRun, ensureAskpassFiles, runHelper,
resolveSudoPassword, unregisterSudoRun, and server.close when making the change.
src/components/shell/view/Shell.tsx (1)

397-445: ⚖️ Poor tradeoff

Consider extracting a shared sudo password modal component.

The inline modal (lines 397-445) duplicates significant structure and styling from SudoPasswordModal.tsx. While the state shapes differ (Shell uses sudoPrompt: string vs. chat uses PendingSudoRequest), the UI layer (~48 lines of JSX) is nearly identical. Extracting a presentational component that accepts { prompt, password, onChange, onSubmit, onCancel } would reduce duplication and simplify future modal updates.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/shell/view/Shell.tsx` around lines 397 - 445, Extract the
inline modal JSX in Shell.tsx (the block guarded by sudoPrompt && isConnected)
into a reusable presentational component (e.g., SudoPasswordPresentational) that
accepts props { prompt, password, onChange, onSubmit, onCancel, inputRef } and
reuses the same styling/structure as SudoPasswordModal.tsx; then replace the
inline block with that component wired to Shell's local state and handlers
(sudoPrompt, sudoPassword, setSudoPassword via onChange, submitSudoPassword as
onSubmit, cancelSudoPrompt as onCancel, sudoInputRef as inputRef). Ensure the
new component is used by the existing SudoPasswordModal.tsx (or imported there)
to avoid duplication and keep Shell.tsx focused on state/logic only.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@server/modules/websocket/utils/sudo-prompt.test.ts`:
- Around line 8-29: Implement the actual detection inside isSudoPasswordPrompt
in server/modules/websocket/utils/sudo-prompt.ts: strip ANSI escape sequences,
split the input into lines and consider the last non-empty line, then test that
line against a regex that matches "[sudo] password for <username>:", "[sudo]
password:" and the Polish variant "[sudo] hasło użytkownika <username>:" (use a
case-insensitive/Unicode-aware pattern and allow any username token). Replace
the current stub that always returns false with this logic so the tests (and
wrapped/preceded-output cases) pass.

---

Outside diff comments:
In `@server/opencode-cli.js`:
- Around line 205-212: The code references an undeclared spawnEnv and then
ignores the merged env when calling spawnFunction; declare and populate a
spawnEnv object from process.env, merge in the sudoRun.env returned by
registerSudoRun (sudoRun and registerSudoRun are the relevant symbols) using
Object.assign or spread, and pass that merged spawnEnv as the env option to
spawnFunction when creating opencodeProcess so the sudo askpass variables are
injected and no ReferenceError occurs.

---

Nitpick comments:
In `@server/modules/sudo-askpass/askpass-integration.test.ts`:
- Around line 53-77: The test currently registers a sudo run and starts an HTTP
server but only calls unregisterSudoRun(token) and server.close() on the success
path; wrap the main test logic (everything after startServer()/registerSudoRun()
and before teardown) in a try/finally block so that unregisterSudoRun(token) and
awaiting server.close() always run in the finally clause, ensuring resources
started by startServer(), registerSudoRun(), and
ensureAskpassFiles()/runHelper() are cleaned up even on assertion failures;
reference the existing symbols startServer, setServerPort, registerSudoRun,
ensureAskpassFiles, runHelper, resolveSudoPassword, unregisterSudoRun, and
server.close when making the change.

In `@src/components/shell/view/Shell.tsx`:
- Around line 397-445: Extract the inline modal JSX in Shell.tsx (the block
guarded by sudoPrompt && isConnected) into a reusable presentational component
(e.g., SudoPasswordPresentational) that accepts props { prompt, password,
onChange, onSubmit, onCancel, inputRef } and reuses the same styling/structure
as SudoPasswordModal.tsx; then replace the inline block with that component
wired to Shell's local state and handlers (sudoPrompt, sudoPassword,
setSudoPassword via onChange, submitSudoPassword as onSubmit, cancelSudoPrompt
as onCancel, sudoInputRef as inputRef). Ensure the new component is used by the
existing SudoPasswordModal.tsx (or imported there) to avoid duplication and keep
Shell.tsx focused on state/logic only.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: dc4a0465-4e43-4825-ba62-8dfe389ec88e

📥 Commits

Reviewing files that changed from the base of the PR and between dd77649 and afa328d.

📒 Files selected for processing (21)
  • server/claude-sdk.js
  • server/cursor-cli.js
  • server/gemini-cli.js
  • server/index.js
  • server/modules/sudo-askpass/askpass-integration.test.ts
  • server/modules/sudo-askpass/sudo-askpass.service.js
  • server/modules/sudo-askpass/sudo-askpass.service.test.ts
  • server/modules/websocket/services/chat-websocket.service.ts
  • server/modules/websocket/utils/sudo-prompt.test.ts
  • server/modules/websocket/utils/sudo-prompt.ts
  • server/opencode-cli.js
  • server/routes/internal-askpass.js
  • server/shared/types.ts
  • src/components/chat/hooks/useSudoPasswordPrompt.ts
  • src/components/chat/view/ChatInterface.tsx
  • src/components/chat/view/subcomponents/SudoPasswordModal.tsx
  • src/components/shell/utils/sudo.test.ts
  • src/components/shell/utils/sudo.ts
  • src/components/shell/view/Shell.tsx
  • src/i18n/locales/en/chat.json
  • src/stores/useSessionStore.ts

Comment on lines +8 to +29
test('detects the default English sudo prompt', () => {
assert.equal(isSudoPasswordPrompt('[sudo] password for piotr: '), true);
});

test('detects the Polish-locale sudo prompt', () => {
assert.equal(isSudoPasswordPrompt('[sudo] hasło użytkownika piotr: '), true);
});

test('detects the generic [sudo] password prompt without a username', () => {
assert.equal(isSudoPasswordPrompt('[sudo] password: '), true);
});

test('detects the prompt when wrapped in ANSI colour escapes', () => {
assert.equal(isSudoPasswordPrompt('\x1b[0m[sudo] password for piotr: \x1b[0m'), true);
});

test('detects the prompt when it is the last line after earlier output', () => {
assert.equal(
isSudoPasswordPrompt('Reading package lists...\r\n[sudo] password for piotr: '),
true
);
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Positive prompt-detection assertions are incompatible with the current implementation stub.

These expectations cannot pass right now: server/modules/websocket/utils/sudo-prompt.ts (Line 1-10) still returns false for all input. Please ship the detector implementation in the same PR (or mark these as pending) so CI stays green.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/modules/websocket/utils/sudo-prompt.test.ts` around lines 8 - 29,
Implement the actual detection inside isSudoPasswordPrompt in
server/modules/websocket/utils/sudo-prompt.ts: strip ANSI escape sequences,
split the input into lines and consider the last non-empty line, then test that
line against a regex that matches "[sudo] password for <username>:", "[sudo]
password:" and the Polish variant "[sudo] hasło użytkownika <username>:" (use a
case-insensitive/Unicode-aware pattern and allow any username token). Replace
the current stub that always returns false with this logic so the tests (and
wrapped/preceded-output cases) pass.

Object.assign(spawnEnv, ...) referenced an undeclared variable causing
a ReferenceError at runtime. Also, sudoRun.env was never passed to the
spawned process. Replace with const spawnEnv = { ...process.env, ...sudoRun.env }
and pass it as the env option.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@blackmammoth

Copy link
Copy Markdown
Member

Hey @szmidtpiotr, thanks for the PR. I understand the feature and how it can be useful to some users, but this makes things overly complicated for most users. So, I'm closing this. If we get enough requests for this, we will circle back on it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants