mirror of
https://github.com/PR0M3TH3AN/SeedPass.git
synced 2026-11-03 19:34:45 +00:00
fix: harden legacy/tui3 non-tty flows and shutdown stability
This commit is contained in:
1 parent
d3fc572c4e
commit
033870f2df
9 files changed
+112
-4
No files matched your search
@@ -6,3 +6,6 @@
|
||||
{"id":"SeedPass-0806","title":"Overstory mail collaboration smoke after codex patch","status":"in_progress","type":"task","priority":2,"createdAt":"2026-03-04T01:45:50.381Z","updatedAt":"2026-03-04T01:45:58.388Z","description":"Verify >=2 agents can exchange ov mail in worktrees after codex runtime add-dir patch"}
|
||||
{"id":"SeedPass-13df","title":"10-agent Overstory launch smoke","status":"in_progress","type":"task","priority":2,"createdAt":"2026-03-04T01:47:19.005Z","updatedAt":"2026-03-04T01:47:29.633Z","description":"Verify 10 codex builders launch concurrently with patched shared-state add-dir"}
|
||||
{"id":"SeedPass-a162","title":"10-agent extra slot","status":"in_progress","type":"task","priority":2,"createdAt":"2026-03-04T01:48:02.794Z","updatedAt":"2026-03-04T01:48:03.781Z","description":"extra slot for 10-agent concurrency smoke"}
|
||||
{"id":"SeedPass-1d93","title":"Installed beta legacy CLI harness exits with SIGSEGV (-11) after graceful exit","status":"in_progress","type":"bug","priority":1,"createdAt":"2026-03-04T02:36:25.559Z","updatedAt":"2026-03-04T02:56:43.538Z","description":"Repro date: 2026-03-04.\n1) Install beta into isolated HOME:\n HOME=/tmp/seedpass-beta-blackbox-1772591563 PATH=/tmp/seedpass-beta-blackbox-1772591563/.local/bin:$PATH bash scripts/install.sh -b beta -m tui\n2) Run deterministic harness against installed app:\n APP_ROOT=/tmp/seedpass-beta-blackbox-1772591563/.seedpass/app\n PY_BIN=$APP_ROOT/venv/bin/python\n HOME=/tmp/seedpass-beta-blackbox-1772591563 $PY_BIN scripts/ai_tui_agent_test.py --scenario core --repo-root $APP_ROOT --python-bin $PY_BIN --output-dir artifacts/agent_tui_test_installed --keep-home\nObserved: harness scenario passes, transcript ends with 'Exiting the program.', but process exit code is -11 (SIGSEGV).\nEvidence: artifacts/agent_tui_test_installed/20260304T023442Z/report.json and artifacts/agent_tui_test_installed/20260304T023503Z/report.json.\nExpected: process exits with code 0 after graceful exit."}
|
||||
{"id":"SeedPass-6d26","title":"seedpass tui3 preflight fails in non-interactive runs with EOF before launch","status":"in_progress","type":"bug","priority":2,"createdAt":"2026-03-04T02:36:25.606Z","updatedAt":"2026-03-04T02:56:43.595Z","description":"Repro date: 2026-03-04.\nFresh install, no existing profile:\nHOME=/tmp/seedpass-beta-blackbox-1772591563 PATH=/tmp/seedpass-beta-blackbox-1772591563/.local/bin:$PATH timeout 8s seedpass tui3\nObserved: command prompts initial seed setup and exits with \"TUI v3 preflight failed: EOF when reading a line\" when stdin is non-interactive.\nExpected: explicit non-interactive failure behavior for automation (single concise error), or add a tui3 check mode similar to tui2 --check."}
|
||||
{"id":"SeedPass-36fb","title":"Non-TTY password prompt for tui3 emits repeated tracebacks and loops on EOF","status":"in_progress","type":"bug","priority":2,"createdAt":"2026-03-04T02:36:25.651Z","updatedAt":"2026-03-04T02:56:43.647Z","description":"Repro date: 2026-03-04.\nWith an existing profile, run in non-tty context:\nHOME=/tmp/seedpass-beta-blackbox-1772591563 PATH=/tmp/seedpass-beta-blackbox-1772591563/.local/bin:$PATH timeout 8s seedpass tui3 > /tmp/tui3_after_bootstrap_probe.txt 2>&1\nObserved: repeated tracebacks from termios fallback and EOF, prompt loops until timeout.\nKey errors include:\n- termios.error: (25, Inappropriate ioctl for device)\n- EOFError: EOF when reading a line\nExpected: single concise non-interactive/TTY-required error and clean exit without traceback spam loops."}
|
||||
+38
-1
@@ -1709,6 +1709,15 @@ def main(argv: list[str] | None = None, *, fingerprint: str | None = None) -> in
|
||||
print(colored(f"Error: Failed to restore backup: {e}", "red"))
|
||||
return 1
|
||||
elif args.command is None and not args.legacy_tui:
|
||||
if not sys.stdin.isatty():
|
||||
print(
|
||||
colored(
|
||||
"Error: Interactive startup requires a TTY. "
|
||||
"Run in a terminal or use non-interactive subcommands.",
|
||||
"red",
|
||||
)
|
||||
)
|
||||
return 1
|
||||
print("Startup Options:")
|
||||
print("1. Continue")
|
||||
print("2. Restore from backup")
|
||||
@@ -1895,5 +1904,33 @@ def main(argv: list[str] | None = None, *, fingerprint: str | None = None) -> in
|
||||
return 0
|
||||
|
||||
|
||||
def _exit_as_main_process() -> "NoReturn":
|
||||
"""Execute ``main`` and terminate the process without Python finalizers.
|
||||
|
||||
In some runtime builds with native extensions, interpreter teardown can
|
||||
intermittently raise SIGSEGV after the app has already completed a normal
|
||||
shutdown path. Using ``os._exit`` here avoids that nondeterministic
|
||||
interpreter-finalization crash while preserving the intended exit status.
|
||||
"""
|
||||
exit_code = 0
|
||||
try:
|
||||
exit_code = int(main())
|
||||
except SystemExit as exc:
|
||||
code = exc.code
|
||||
if isinstance(code, int):
|
||||
exit_code = code
|
||||
elif code is None:
|
||||
exit_code = 0
|
||||
else:
|
||||
exit_code = 1
|
||||
finally:
|
||||
try:
|
||||
sys.stdout.flush()
|
||||
sys.stderr.flush()
|
||||
except Exception:
|
||||
pass
|
||||
os._exit(exit_code)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
_exit_as_main_process()
|
||||
@@ -302,8 +302,21 @@ def tui2(
|
||||
|
||||
|
||||
@app.command("tui3")
|
||||
def tui3(ctx: typer.Context) -> None:
|
||||
def tui3(
|
||||
ctx: typer.Context,
|
||||
check: bool = typer.Option(
|
||||
False,
|
||||
"--check",
|
||||
help="Check whether TUI v3 runtime dependencies are available",
|
||||
),
|
||||
) -> None:
|
||||
"""Launch the experimental TUI v3 scratch architecture."""
|
||||
if check:
|
||||
from seedpass.tui_v3 import check_tui3_runtime
|
||||
|
||||
typer.echo(json.dumps(check_tui3_runtime(), indent=2, sort_keys=True))
|
||||
return
|
||||
|
||||
fingerprint = (ctx.obj or {}).get("fingerprint")
|
||||
services, preflight_error = _prime_tui2_service(ctx)
|
||||
if preflight_error is not None:
|
||||
|
||||
@@ -63,6 +63,7 @@ from utils.password_prompt import (
|
||||
prompt_existing_password,
|
||||
prompt_new_password,
|
||||
confirm_action,
|
||||
PasswordPromptError,
|
||||
)
|
||||
from utils import masked_input, prompt_seed_words
|
||||
from utils.memory_protection import InMemorySecret
|
||||
@@ -731,6 +732,10 @@ class PasswordManager:
|
||||
)
|
||||
print(colored("Q. Exit", "cyan"))
|
||||
|
||||
if not sys.stdin.isatty():
|
||||
raise SeedPassError(
|
||||
"Interactive seed profile selection requires a TTY"
|
||||
)
|
||||
choice = input("Select a seed profile by number: ").strip()
|
||||
if choice.lower() in {"q", "quit", "exit"}:
|
||||
raise SeedPassError("Operation cancelled by user")
|
||||
@@ -766,7 +771,14 @@ class PasswordManager:
|
||||
current = self.fingerprint_manager.current_fingerprint
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error during seed profile selection: {e}", exc_info=True)
|
||||
is_interactive_err = isinstance(e, (EOFError, PasswordPromptError)) or (
|
||||
isinstance(e, SeedPassError) and "requires a TTY" in str(e)
|
||||
)
|
||||
logger.error(
|
||||
"Error during seed profile selection: %s",
|
||||
e,
|
||||
exc_info=not is_interactive_err,
|
||||
)
|
||||
print(colored(f"Error: Failed to select seed profile: {e}", "red"))
|
||||
raise SeedPassError(f"Failed to select seed profile: {e}") from e
|
||||
|
||||
@@ -1094,7 +1106,12 @@ class PasswordManager:
|
||||
except KeyboardInterrupt:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to set up EncryptionManager: {e}", exc_info=True)
|
||||
is_interactive_err = isinstance(e, (EOFError, PasswordPromptError))
|
||||
logger.error(
|
||||
"Failed to set up EncryptionManager: %s",
|
||||
e,
|
||||
exc_info=not is_interactive_err,
|
||||
)
|
||||
print(colored(f"Error: Failed to set up encryption: {e}", "red"))
|
||||
if exit_on_fail:
|
||||
raise SeedPassError(f"Failed to set up encryption: {e}") from e
|
||||
|
||||
@@ -28,6 +28,16 @@ def test_prompt_existing_password(monkeypatch):
|
||||
assert password_prompt.prompt_existing_password() == "mypassword"
|
||||
|
||||
|
||||
def test_prompt_existing_password_non_tty(monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
password_prompt,
|
||||
"masked_input",
|
||||
lambda _prompt: (_ for _ in ()).throw(EOFError("no tty")),
|
||||
)
|
||||
with pytest.raises(password_prompt.PasswordPromptError, match="requires a TTY"):
|
||||
password_prompt.prompt_existing_password()
|
||||
|
||||
|
||||
def test_confirm_action_yes_no(monkeypatch):
|
||||
monkeypatch.setattr(builtins, "input", lambda _: "Y")
|
||||
assert password_prompt.confirm_action()
|
||||
|
||||
@@ -5,6 +5,7 @@ from utils import seed_prompt
|
||||
|
||||
def test_masked_input_posix_backspace(monkeypatch, capsys):
|
||||
seq = iter(["a", "b", "\x7f", "c", "\n"])
|
||||
monkeypatch.setattr(seed_prompt.sys.stdin, "isatty", lambda: True)
|
||||
monkeypatch.setattr(seed_prompt.sys.stdin, "read", lambda n=1: next(seq))
|
||||
monkeypatch.setattr(seed_prompt.sys.stdin, "fileno", lambda: 0)
|
||||
|
||||
@@ -36,6 +37,7 @@ def test_masked_input_posix_backspace(monkeypatch, capsys):
|
||||
|
||||
def test_masked_input_windows_space(monkeypatch, capsys):
|
||||
seq = iter(["x", "y", " ", "z", "\r"])
|
||||
monkeypatch.setattr(seed_prompt.sys.stdin, "isatty", lambda: True)
|
||||
fake_msvcrt = types.SimpleNamespace(getwch=lambda: next(seq))
|
||||
monkeypatch.setattr(seed_prompt, "msvcrt", fake_msvcrt)
|
||||
monkeypatch.setattr(seed_prompt.sys, "platform", "win32", raising=False)
|
||||
@@ -49,6 +51,7 @@ def test_masked_input_windows_space(monkeypatch, capsys):
|
||||
|
||||
def test_masked_input_posix_ctrl_c(monkeypatch):
|
||||
seq = iter(["\x03"])
|
||||
monkeypatch.setattr(seed_prompt.sys.stdin, "isatty", lambda: True)
|
||||
monkeypatch.setattr(seed_prompt.sys.stdin, "read", lambda n=1: next(seq))
|
||||
monkeypatch.setattr(seed_prompt.sys.stdin, "fileno", lambda: 0)
|
||||
|
||||
@@ -70,6 +73,7 @@ def test_masked_input_posix_ctrl_c(monkeypatch):
|
||||
|
||||
def test_masked_input_windows_ctrl_c(monkeypatch):
|
||||
seq = iter(["\x03"])
|
||||
monkeypatch.setattr(seed_prompt.sys.stdin, "isatty", lambda: True)
|
||||
fake_msvcrt = types.SimpleNamespace(getwch=lambda: next(seq))
|
||||
monkeypatch.setattr(seed_prompt, "msvcrt", fake_msvcrt)
|
||||
monkeypatch.setattr(seed_prompt.sys, "platform", "win32", raising=False)
|
||||
@@ -93,6 +97,12 @@ def test_prompt_seed_words_valid(monkeypatch):
|
||||
assert result == phrase
|
||||
|
||||
|
||||
def test_masked_input_requires_tty(monkeypatch):
|
||||
monkeypatch.setattr(seed_prompt.sys.stdin, "isatty", lambda: False)
|
||||
with pytest.raises(EOFError, match="requires a TTY"):
|
||||
seed_prompt.masked_input("Enter: ")
|
||||
|
||||
|
||||
def test_prompt_seed_words_invalid_word(monkeypatch):
|
||||
from mnemonic import Mnemonic
|
||||
|
||||
|
||||
@@ -1092,6 +1092,18 @@ def test_tui2_check(monkeypatch):
|
||||
assert payload["textual_available"] is True
|
||||
|
||||
|
||||
def test_tui3_check(monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
cli,
|
||||
"check_tui2_runtime",
|
||||
lambda: {"status": "ok", "backend": "textual", "textual_available": True},
|
||||
)
|
||||
result = runner.invoke(app, ["tui3", "--check"])
|
||||
assert result.exit_code == 0
|
||||
payload = json.loads(result.stdout)
|
||||
assert "textual_available" in payload
|
||||
|
||||
|
||||
def test_tui2_unavailable_without_fallback(monkeypatch):
|
||||
monkeypatch.setattr(cli, "_get_entry_service", lambda _ctx: object())
|
||||
monkeypatch.setattr(cli, "launch_tui2", lambda **_: False)
|
||||
|
||||
@@ -142,6 +142,8 @@ def prompt_new_password(max_retries: int | None = None) -> str:
|
||||
print(colored("\nOperation cancelled by user.", "yellow"))
|
||||
logging.info("Password prompt interrupted by user.")
|
||||
raise PasswordPromptError("Operation cancelled by user")
|
||||
except EOFError:
|
||||
raise PasswordPromptError("Interactive password prompt requires a TTY")
|
||||
except Exception as e:
|
||||
logging.error(
|
||||
f"Unexpected error during password prompt: {e}", exc_info=True
|
||||
@@ -204,6 +206,8 @@ def prompt_existing_password(
|
||||
print(colored("\nOperation cancelled by user.", "yellow"))
|
||||
logging.info("Existing password prompt interrupted by user.")
|
||||
raise PasswordPromptError("Operation cancelled by user")
|
||||
except EOFError:
|
||||
raise PasswordPromptError("Interactive password prompt requires a TTY")
|
||||
except Exception as e:
|
||||
logging.error(
|
||||
f"Unexpected error during existing password prompt: {e}",
|
||||
|
||||
@@ -106,6 +106,8 @@ def _masked_input_posix(prompt: str) -> str:
|
||||
|
||||
def masked_input(prompt: str) -> str:
|
||||
"""Return input from the user while masking typed characters."""
|
||||
if not sys.stdin.isatty():
|
||||
raise EOFError("Interactive input requires a TTY")
|
||||
func = _masked_input_windows if sys.platform == "win32" else _masked_input_posix
|
||||
pause_console_logging()
|
||||
try:
|
||||
|
||||
Reference in new issue
Block a user