@xynogen/pix-ssh 0.4.1 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -26,7 +26,7 @@ Lets the agent discover hosts without reading `~/.ssh/config` itself. Read-only:
26
26
  ### Authentication
27
27
 
28
28
  - **SSH login** — tries key/agent/existing-master auth first via a `BatchMode=yes` probe. If that succeeds, no login password is needed. If it fails, a masked overlay (`●` per character) collects the login password, fed to `sshpass -e` through the child's `SSHPASS` env var — never as an argv (no `ps` leak), never written to disk.
29
- - **Remote sudo** (`sudo: true`) — a separate masked prompt collects the remote sudo password, piped to the remote `sudo -S -p ''` on stdin, so it travels inside the encrypted SSH channel, not as an argv.
29
+ - **Remote sudo** (`sudo: true`) — when the SSH connection is passwordless, `ssh_run` first probes `sudo -n true`; if the remote sudoers is `NOPASSWD` the overlay shows a plain Allow/Deny with no sudo prompt. Otherwise a separate masked prompt collects the remote sudo password, piped to the remote `sudo -S -p ''` on stdin, so it travels inside the encrypted SSH channel, not as an argv.
30
30
 
31
31
  Both passwords are cached **in-memory per host** for the session (keyed by `user@host:port`), never persisted. A wrong login password drops the login cache; a wrong sudo password drops the sudo cache — the next call re-prompts.
32
32
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xynogen/pix-ssh",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
4
4
  "description": "Pi tool — ssh_run: run remote commands over SSH with password/key auth and remote sudo",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
package/src/index.ts CHANGED
@@ -66,6 +66,7 @@ import {
66
66
  MAX_OUTPUT_LINES,
67
67
  parseHost,
68
68
  probeKeyAuth,
69
+ probeSudoNoPassword,
69
70
  readSshConfigAliases,
70
71
  resolveHostInfo,
71
72
  resolveSshHost,
@@ -536,7 +537,15 @@ export default function (pi: ExtensionAPI): void {
536
537
  const keyOk = probe === "ok";
537
538
  // Which passwords must the overlay collect this call?
538
539
  const needLogin = probe === "auth" && !creds.loginPassword;
539
- const needSudo = sudo && !creds.sudoPassword;
540
+ // Remote sudo password only when NOT already cached, NOT NOPASSWD, and
541
+ // the SSH connection is passwordless (so the probe can open its own
542
+ // channel without prompting). When login still needs a password we
543
+ // can't probe yet, so fall back to prompting for the sudo password.
544
+ const sudoNoPassword =
545
+ sudo && !creds.sudoPassword && !needLogin && (keyOk || Boolean(creds.loginPassword))
546
+ ? await probeSudoNoPassword(spec, controlPath, sig)
547
+ : false;
548
+ const needSudo = sudo && !creds.sudoPassword && !sudoNoPassword;
540
549
  // The overlay stage pipeline: any password we still need is prompted
541
550
  // (login first, then sudo). Confirm-only when nothing is missing.
542
551
  const promptFor: ("login" | "sudo")[] = [
package/src/lib.ts CHANGED
@@ -454,6 +454,33 @@ export function probeKeyAuth(
454
454
  });
455
455
  }
456
456
 
457
+ /**
458
+ * Probe whether remote sudo runs without a password (NOPASSWD sudoers) via
459
+ * `sudo -n true`. Returns true when no sudo password is needed. Requires a
460
+ * passwordless SSH connection (key/agent/existing-master or a cached login
461
+ * password) — the probe opens its own channel with `BatchMode=yes`, so callers
462
+ * must only probe when SSH login auth already succeeds without prompting.
463
+ */
464
+ export function probeSudoNoPassword(
465
+ spec: HostSpec,
466
+ controlPath: string,
467
+ signal?: AbortSignal,
468
+ ): Promise<boolean> {
469
+ const args = [
470
+ ...baseSshArgs(spec, controlPath),
471
+ "-o",
472
+ "BatchMode=yes",
473
+ hostTarget(spec),
474
+ "sudo -n true",
475
+ ];
476
+ return new Promise((resolve) => {
477
+ const proc = spawn("ssh", args, { stdio: ["ignore", "ignore", "ignore"] });
478
+ proc.on("error", () => resolve(false));
479
+ proc.on("close", (code) => resolve(code === 0));
480
+ signal?.addEventListener("abort", () => proc.kill("SIGTERM"), { once: true });
481
+ });
482
+ }
483
+
457
484
  /** Connection/DNS-level failure (not an auth rejection) — a password can't fix it. */
458
485
  export function isUnreachable(stderr: string): boolean {
459
486
  const lower = stderr.toLowerCase();