fix: harden legacy/tui3 non-tty flows and shutdown stability

This commit is contained in:
thePR0M3TH3AN committed 2026-03-03 21:56:55 -05:00
1 parent d3fc572c4e
commit 033870f2df
9 files changed
+112 -4

No files matched your search

+3
View File
@@ -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
View File
@@ -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()
+14 -1
View File
@@ -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:
+19 -2
View File
@@ -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
+10
View File
@@ -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()
+10
View File
@@ -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
+12
View File
@@ -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)
+4
View File
@@ -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}",
+2
View File
@@ -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: