feat: add tested Termux install path and EOF-aware gh auth

This commit is contained in:
adybag14-cyber
2026-04-08 17:48:25 +02:00
committed by Teknium
parent e053433c84
commit e79cc88985
15 changed files with 724 additions and 23 deletions

View File

@@ -700,6 +700,29 @@ class ProcessRegistry:
"""Send data + newline to a running process's stdin (like pressing Enter)."""
return self.write_stdin(session_id, data + "\n")
def close_stdin(self, session_id: str) -> dict:
"""Close a running process's stdin / send EOF without killing the process."""
session = self.get(session_id)
if session is None:
return {"status": "not_found", "error": f"No process with ID {session_id}"}
if session.exited:
return {"status": "already_exited", "error": "Process has already finished"}
if hasattr(session, '_pty') and session._pty:
try:
session._pty.sendeof()
return {"status": "ok", "message": "EOF sent"}
except Exception as e:
return {"status": "error", "error": str(e)}
if not session.process or not session.process.stdin:
return {"status": "error", "error": "Process stdin not available (non-local backend or stdin closed)"}
try:
session.process.stdin.close()
return {"status": "ok", "message": "stdin closed"}
except Exception as e:
return {"status": "error", "error": str(e)}
def list_sessions(self, task_id: str = None) -> list:
"""List all running and recently-finished processes."""
with self._lock:
@@ -915,14 +938,14 @@ PROCESS_SCHEMA = {
"Actions: 'list' (show all), 'poll' (check status + new output), "
"'log' (full output with pagination), 'wait' (block until done or timeout), "
"'kill' (terminate), 'write' (send raw stdin data without newline), "
"'submit' (send data + Enter, for answering prompts)."
"'submit' (send data + Enter, for answering prompts), 'close' (close stdin/send EOF)."
),
"parameters": {
"type": "object",
"properties": {
"action": {
"type": "string",
"enum": ["list", "poll", "log", "wait", "kill", "write", "submit"],
"enum": ["list", "poll", "log", "wait", "kill", "write", "submit", "close"],
"description": "Action to perform on background processes"
},
"session_id": {
@@ -962,7 +985,7 @@ def _handle_process(args, **kw):
if action == "list":
return _json.dumps({"processes": process_registry.list_sessions(task_id=task_id)}, ensure_ascii=False)
elif action in ("poll", "log", "wait", "kill", "write", "submit"):
elif action in ("poll", "log", "wait", "kill", "write", "submit", "close"):
if not session_id:
return tool_error(f"session_id is required for {action}")
if action == "poll":
@@ -978,7 +1001,9 @@ def _handle_process(args, **kw):
return _json.dumps(process_registry.write_stdin(session_id, str(args.get("data", ""))), ensure_ascii=False)
elif action == "submit":
return _json.dumps(process_registry.submit_stdin(session_id, str(args.get("data", ""))), ensure_ascii=False)
return tool_error(f"Unknown process action: {action}. Use: list, poll, log, wait, kill, write, submit")
elif action == "close":
return _json.dumps(process_registry.close_stdin(session_id), ensure_ascii=False)
return tool_error(f"Unknown process action: {action}. Use: list, poll, log, wait, kill, write, submit, close")
registry.register(

View File

@@ -1112,6 +1112,21 @@ def _interpret_exit_code(command: str, exit_code: int) -> str | None:
return None
def _command_requires_pipe_stdin(command: str) -> bool:
"""Return True when PTY mode would break stdin-driven commands.
Some CLIs change behavior when stdin is a TTY. In particular,
`gh auth login --with-token` expects the token to arrive via piped stdin and
waits for EOF; when we launch it under a PTY, `process.submit()` only sends a
newline, so the command appears to hang forever with no visible progress.
"""
normalized = " ".join(command.lower().split())
return (
normalized.startswith("gh auth login")
and "--with-token" in normalized
)
def terminal_tool(
command: str,
background: bool = False,
@@ -1332,6 +1347,17 @@ def terminal_tool(
}, ensure_ascii=False)
# Prepare command for execution
pty_disabled_reason = None
effective_pty = pty
if pty and _command_requires_pipe_stdin(command):
effective_pty = False
pty_disabled_reason = (
"PTY disabled for this command because it expects piped stdin/EOF "
"(for example gh auth login --with-token). For local background "
"processes, call process(action='close') after writing so it receives "
"EOF."
)
if background:
# Spawn a tracked background process via the process registry.
# For local backends: uses subprocess.Popen with output buffering.
@@ -1349,7 +1375,7 @@ def terminal_tool(
task_id=effective_task_id,
session_key=session_key,
env_vars=env.env if hasattr(env, 'env') else None,
use_pty=pty,
use_pty=effective_pty,
)
else:
proc_session = process_registry.spawn_via_env(
@@ -1369,6 +1395,8 @@ def terminal_tool(
}
if approval_note:
result_data["approval"] = approval_note
if pty_disabled_reason:
result_data["pty_note"] = pty_disabled_reason
# Transparent timeout clamping note
max_timeout = effective_timeout