askpass.py 3.0 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879
  1. """在没有终端的环境里,用操作系统的密码对话框读取密码。
  2. 为什么需要它:Claude Desktop、IDE 集成、agent 代跑等场景都没有 TTY,`getpass`
  3. 用不了。而让用户把密码打进对话、或放进命令行参数,都会让密码进入会话上下文或
  4. `ps` / shell history——那是真正的泄漏。
  5. 系统对话框解决了这个矛盾:**密码由用户直接输给操作系统,不经过终端、不经过 argv、
  6. 调用方(包括 agent)全程看不到**,只从子进程的 stdout 拿回来,留在本进程内存里。
  7. 支持 zenity / kdialog(Linux)、osascript(macOS)、PowerShell(Windows)。
  8. 没有图形界面时返回 None,由调用方给出可操作的提示。
  9. """
  10. import os
  11. import shutil
  12. import subprocess
  13. import sys
  14. DIALOG_TIMEOUT = 180
  15. def gui_available():
  16. """当前环境有没有可用的图形界面。"""
  17. if sys.platform == 'darwin':
  18. return shutil.which('osascript') is not None
  19. if os.name == 'nt':
  20. return True
  21. if not (os.environ.get('DISPLAY') or os.environ.get('WAYLAND_DISPLAY')):
  22. return False
  23. return shutil.which('zenity') is not None or shutil.which('kdialog') is not None
  24. def _run(cmd):
  25. """跑对话框命令,返回用户输入;取消或失败返回 None。
  26. stdout 只在本函数内流转,绝不打印——它装着密码。
  27. """
  28. try:
  29. proc = subprocess.run(cmd, capture_output=True, text=True, timeout=DIALOG_TIMEOUT)
  30. except (OSError, subprocess.TimeoutExpired):
  31. return None
  32. if proc.returncode != 0:
  33. return None
  34. return proc.stdout.rstrip('\n')
  35. def ask(prompt, title='WikiPali', secret=True):
  36. """弹一个对话框问用户要一行输入。secret=True 时输入被遮蔽。
  37. 返回字符串;用户取消、超时或没有图形界面时返回 None。
  38. """
  39. if sys.platform == 'darwin':
  40. hidden = ' with hidden answer' if secret else ''
  41. script = (f'display dialog "{prompt}" with title "{title}" '
  42. f'default answer ""{hidden}')
  43. out = _run(['osascript', '-e', script, '-e',
  44. 'text returned of result'])
  45. return out
  46. if os.name == 'nt':
  47. if secret:
  48. ps = (f'$s = Read-Host -AsSecureString "{prompt}"; '
  49. '[Runtime.InteropServices.Marshal]::PtrToStringAuto('
  50. '[Runtime.InteropServices.Marshal]::SecureStringToBSTR($s))')
  51. else:
  52. ps = f'Read-Host "{prompt}"'
  53. return _run(['powershell', '-NoProfile', '-Command', ps])
  54. if shutil.which('zenity'):
  55. if secret:
  56. # --password 只给密码框;要用户名时用 --entry
  57. return _run(['zenity', '--password', '--title', f'{title} — {prompt}'])
  58. return _run(['zenity', '--entry', '--title', title, '--text', prompt])
  59. if shutil.which('kdialog'):
  60. flag = '--password' if secret else '--inputbox'
  61. return _run(['kdialog', '--title', title, flag, prompt])
  62. return None