From 1ff34c57bbd819c581bee2665f370d1e1dfa7027 Mon Sep 17 00:00:00 2001 From: Victor Vogel Date: Thu, 15 Oct 2026 20:42:35 -0500 Subject: [PATCH 1/3] test(ssh): cover the prompt text after an SSH auth failure Refs #10784 Co-authored-by: hiimchinh Co-Authored-By: Claude Opus 5.5 --- packages/ssh/src/tunnel.test.ts | 35 +++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/packages/ssh/src/tunnel.test.ts b/packages/ssh/src/tunnel.test.ts index 9bb303d6afef8..5f1f9d9a1731a 100644 --- a/packages/ssh/src/tunnel.test.ts +++ b/packages/ssh/src/tunnel.test.ts @@ -549,6 +549,41 @@ describe("ssh tunnel scripts", () => { }), ); + it.effect("asks for a password or verification code when SSH auth fails", () => + Effect.gen(function* () { + const prompts: Array = []; + const spawner = ChildProcessSpawner.make(() => + Effect.succeed({ + ...makeSuccessfulProcess(""), + exitCode: Effect.succeed(ChildProcessSpawner.ExitCode(255)), + stderr: Stream.make( + new TextEncoder().encode("user@devbox: Permission denied (keyboard-interactive).\n"), + ), + }), + ); + const layer = Layer.mergeAll( + NodeServices.layer, + Layer.succeed(ChildProcessSpawner.ChildProcessSpawner, spawner), + Layer.succeed(HttpClient.HttpClient, testHttpClient), + Layer.succeed(NetService.NetService, testNetService), + Layer.succeed(SshAuth.SshPasswordPrompt, { + isAvailable: true, + request: (request) => + Effect.sync(() => prompts.push(request.prompt)).pipe(Effect.as(null)), + }), + SshTunnel.SshEnvironmentManager.layer({ resolveCliRunner: Effect.succeed(ARCHIVE) }), + ); + yield* Effect.gen(function* () { + const manager = yield* SshTunnel.SshEnvironmentManager; + const target = { alias: "devbox", hostname: "devbox", username: "user", port: null }; + yield* Effect.result(manager.ensureEnvironment(target)); + assert.deepStrictEqual(prompts, [ + "Enter the SSH password or verification code for user@devbox.", + ]); + }).pipe(Effect.provide(layer), Effect.scoped); + }), + ); + it.effect.each(["successful stop", "failed stop"] as const)( "closes the tunnel scope and starts fresh after a %s", (mode) => { From da4138d2deecc730a5b7f0a879f0059604f254b3 Mon Sep 17 00:00:00 2001 From: Victor Vogel Date: Thu, 15 Oct 2026 20:43:09 -0500 Subject: [PATCH 2/3] fix(ssh): ask for a password or verification code, not only a password Servers that require a key and then a one-time code (keyboard-interactive) showed "SSH Password Required" and suggested SSH keys would remove the prompt. Use neutral wording in the dialog and the generated prompt text. Fixes #10784 Co-authored-by: hiimchinh Co-Authored-By: Claude Opus 5.5 --- .../desktop/SshPasswordPromptDialog.tsx | 18 ++++++++++-------- packages/ssh/src/tunnel.ts | 2 +- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/apps/web/src/components/desktop/SshPasswordPromptDialog.tsx b/apps/web/src/components/desktop/SshPasswordPromptDialog.tsx index d3727ae210bdf..c63042e8e57f1 100644 --- a/apps/web/src/components/desktop/SshPasswordPromptDialog.tsx +++ b/apps/web/src/components/desktop/SshPasswordPromptDialog.tsx @@ -22,9 +22,9 @@ function formatRemainingSeconds(seconds: number): string { } function getPromptErrorMessage(error: unknown): string { - const message = error instanceof Error ? error.message : "SSH password prompt failed."; + const message = error instanceof Error ? error.message : "SSH verification prompt failed."; return message.includes("expired") || message.includes("no longer pending") - ? "This SSH password prompt expired. Try connecting again." + ? "This SSH verification prompt expired. Try connecting again." : message; } @@ -101,7 +101,7 @@ function ActiveSshPasswordPrompt({ const remainingLabel = remainingSeconds === null ? null : formatRemainingSeconds(remainingSeconds); const visibleResponseError = isExpired - ? "This SSH password prompt expired. Try connecting again." + ? "This SSH verification prompt expired. Try connecting again." : responseError; const respond = async (nextPassword: string | null) => { @@ -111,7 +111,7 @@ function ActiveSshPasswordPrompt({ const requestId = request.requestId; if (nextPassword !== null && isExpired) { - setResponseError("This SSH password prompt expired. Try connecting again."); + setResponseError("This SSH verification prompt expired. Try connecting again."); return; } @@ -158,10 +158,11 @@ function ActiveSshPasswordPrompt({ > - SSH Password Required + SSH verification required - T3 needs your SSH password to connect to {target}. The password is passed - to the local SSH process for this connection attempt and is not saved by T3 Code. + Enter the password or verification code required by {target}. Your response + is passed to the local SSH process for this connection attempt and is not saved by T3 + Code. @@ -202,7 +203,8 @@ function ActiveSshPasswordPrompt({

{visibleResponseError}

) : (

- Use SSH keys to avoid repeated password prompts on new SSH sessions. + SSH keys may replace a password, but your server can still require a verification + code.

)} diff --git a/packages/ssh/src/tunnel.ts b/packages/ssh/src/tunnel.ts index 472dcfc4188d9..0b66aab83227c 100644 --- a/packages/ssh/src/tunnel.ts +++ b/packages/ssh/src/tunnel.ts @@ -1436,7 +1436,7 @@ const makeSshEnvironmentManager = Effect.fn("ssh/tunnel.SshEnvironmentManager.ma attempt, destination: target.alias.trim() || target.hostname.trim(), username: target.username, - prompt: `Enter the SSH password for ${hostSpec}.`, + prompt: `Enter the SSH password or verification code for ${hostSpec}.`, }); if (password === null) { yield* Effect.logWarning("ssh.auth.passwordPrompt.cancelled", { From 2b946ab635c7beabca1e0beb213eb3e61a73a795 Mon Sep 17 00:00:00 2001 From: Victor Vogel Date: Fri, 16 Oct 2026 10:26:32 -0500 Subject: [PATCH 3/3] fix(ssh): say the prompt response stays in memory, not on disk The dialog said the response was used only for this connection attempt and not saved. The tunnel manager keeps it in memory per target, reuses it for later connects and remote cleanup, and drops it on auth failure. It reaches ssh through an env var and is never written to disk. Co-Authored-By: Claude Opus 5.5 --- apps/web/src/components/desktop/SshPasswordPromptDialog.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/web/src/components/desktop/SshPasswordPromptDialog.tsx b/apps/web/src/components/desktop/SshPasswordPromptDialog.tsx index c63042e8e57f1..c433d67493308 100644 --- a/apps/web/src/components/desktop/SshPasswordPromptDialog.tsx +++ b/apps/web/src/components/desktop/SshPasswordPromptDialog.tsx @@ -160,9 +160,9 @@ function ActiveSshPasswordPrompt({ SSH verification required - Enter the password or verification code required by {target}. Your response - is passed to the local SSH process for this connection attempt and is not saved by T3 - Code. + Enter the password or verification code required by {target}. T3 Code + passes your response to the local SSH process and keeps it in memory to reuse for this + host. It is never written to disk.