mirror of
https://github.com/usestrix/strix.git
synced 2026-10-26 06:02:30 +00:00
Merge 3c98245c14 into fde37a149f
This commit is contained in:
commit
d7d55ffa99
4 changed files with 40 additions and 39 deletions
|
|
@ -446,35 +446,29 @@ _EXEC_YIELD_CAP_S = PTY_YIELD_TIME_MS_MAX // 1000
|
|||
|
||||
_EXEC_COMMAND_DESCRIPTION_NOTE = (
|
||||
f"The wait for output is capped at {_EXEC_YIELD_CAP_S} s whatever yield_time_ms says. "
|
||||
"A non-TTY command still running at the yield cannot be read later (write_stdin fails "
|
||||
"on it and its output is lost): run anything longer in the background with its output "
|
||||
"redirected to a file (`cmd > /tmp/out.log 2>&1 &`, then read the log), or start it with "
|
||||
'tty=true and poll with write_stdin(session_id=<id>, chars="").'
|
||||
)
|
||||
|
||||
_RUNNING_NON_TTY_NOTICE = (
|
||||
"Note: this non-TTY process cannot be polled and its later output is lost. Rerun it in "
|
||||
"the background (`cmd > /tmp/out.log 2>&1 &`, then read the log) or with tty=true."
|
||||
"A command still running at the yield keeps running: read the rest of its output and "
|
||||
'its exit code with write_stdin(session_id=<id>, chars="").'
|
||||
)
|
||||
|
||||
_PROCESS_RUNNING_RE = re.compile(r"^Process running with session ID \d+$", re.MULTILINE)
|
||||
_OUTPUT_HEADER_RE = re.compile(r"^Output:\n", re.MULTILINE)
|
||||
|
||||
|
||||
def _annotate_running_non_tty(parsed: dict[str, Any], result: Any) -> Any:
|
||||
if parsed.get("tty") or not isinstance(result, str):
|
||||
def _annotate_capped_yield(parsed: dict[str, Any], result: Any) -> Any:
|
||||
requested = parsed.get("yield_time_ms")
|
||||
if not isinstance(result, str) or not isinstance(requested, int | float):
|
||||
return result
|
||||
if requested <= PTY_YIELD_TIME_MS_MAX:
|
||||
return result
|
||||
header = _OUTPUT_HEADER_RE.search(result)
|
||||
head_end = len(result) if header is None else header.start()
|
||||
if _PROCESS_RUNNING_RE.search(result, 0, head_end) is None:
|
||||
return result
|
||||
notice = _RUNNING_NON_TTY_NOTICE
|
||||
requested = parsed.get("yield_time_ms")
|
||||
if isinstance(requested, int | float) and requested > PTY_YIELD_TIME_MS_MAX:
|
||||
notice = (
|
||||
f"Note: yield_time_ms={int(requested)} was capped at {PTY_YIELD_TIME_MS_MAX} "
|
||||
f"({_EXEC_YIELD_CAP_S} s). " + notice.removeprefix("Note: ")
|
||||
)
|
||||
notice = (
|
||||
f"Note: yield_time_ms={int(requested)} was capped at {PTY_YIELD_TIME_MS_MAX} "
|
||||
f"({_EXEC_YIELD_CAP_S} s); the command is still running, read it with "
|
||||
'write_stdin(session_id=<id>, chars="").'
|
||||
)
|
||||
if header is None:
|
||||
return f"{result}\n{notice}"
|
||||
return f"{result[: header.end()]}{notice}\n{result[header.end() :]}"
|
||||
|
|
@ -506,7 +500,7 @@ def _wrap_exec_command(tool: FunctionTool) -> FunctionTool:
|
|||
f"Got: {rel!r}."
|
||||
)
|
||||
if isinstance(parsed, dict):
|
||||
return _annotate_running_non_tty(parsed, result)
|
||||
return _annotate_capped_yield(parsed, result)
|
||||
return result
|
||||
|
||||
tool.on_invoke_tool = invoke
|
||||
|
|
|
|||
|
|
@ -172,12 +172,13 @@ EFFICIENCY TACTICS:
|
|||
(it installs into that active venv) before importing, rather than letting the
|
||||
script fail with `ModuleNotFoundError`.
|
||||
- `exec_command` runs each command in a fresh non-interactive shell (plain
|
||||
pipes, no TTY). To drive an interactive or long-running process with
|
||||
`write_stdin` — REPLs, `ssh`/`nc`/`ftp`, `sqlmap`, or to send Ctrl-C —
|
||||
you MUST start it with `exec_command(cmd="...", tty=true)` and then
|
||||
`write_stdin(session_id=<id>, chars="...")`. Calling `write_stdin` on a
|
||||
default (non-TTY) command or on a process that has already exited fails with
|
||||
"stdin is not available".
|
||||
pipes, no TTY) and waits at most 30 s for output. A command still running
|
||||
at that point keeps running: read the rest of its output and its exit code
|
||||
with `write_stdin(session_id=<id>, chars="")`; never re-run it or wait for
|
||||
it with `sleep`. To type into a process — REPLs, `ssh`/`nc`/`ftp`,
|
||||
`sqlmap` — start it with `exec_command(cmd="...", tty=true)`, then
|
||||
`write_stdin(session_id=<id>, chars="...")`. Typing into a non-TTY command
|
||||
fails with "stdin is not available".
|
||||
- For Caido proxy automation inside Python, explicitly import from
|
||||
`caido_api`:
|
||||
`from caido_api import list_requests, view_request, repeat_request, list_sitemap, view_sitemap_entry, scope_rules`
|
||||
|
|
|
|||
|
|
@ -5,19 +5,27 @@ invocation the agent makes (nmap, ffuf, agent-browser, python3, …) goes
|
|||
through `exec_command`. `write_stdin` streams input to a still-running
|
||||
process started by an earlier `exec_command` (for interactive prompts).
|
||||
|
||||
## `write_stdin` requires a TTY-backed process
|
||||
## Reading a running command and typing into it
|
||||
|
||||
`exec_command` runs each command in a fresh **non-interactive** shell (plain
|
||||
pipes, no TTY) by default. `write_stdin` only works against a process that is
|
||||
still running **and** was started with a PTY. The canonical sequence is:
|
||||
pipes, no TTY) by default and waits at most 30 s for output. A command still
|
||||
running at that point keeps running; read the rest of its output and its exit
|
||||
code with an empty write:
|
||||
|
||||
```text
|
||||
exec_command(cmd="nmap -sV 10.0.0.5") # returns "Process running with session ID <id>"
|
||||
write_stdin(session_id=<id>, chars="") # returns the new output, or the exit code
|
||||
```
|
||||
|
||||
Typing into a process needs a PTY:
|
||||
|
||||
```text
|
||||
exec_command(cmd="python3", tty=true) # start a PTY-backed process
|
||||
write_stdin(session_id=<id>, chars="print(1)\n")
|
||||
```
|
||||
|
||||
Calling `write_stdin` on a command started with the default `tty=false`, or on
|
||||
a process that has already exited, fails with
|
||||
Non-empty `chars` on a command started with the default `tty=false`, or on a
|
||||
process that has already exited, fails with
|
||||
`stdin is not available for this process. Start the command with 'tty=true' in
|
||||
'exec_command' before using 'write_stdin'.` Use `tty=true` for REPLs,
|
||||
`ssh`/`nc`/`ftp`, `msfconsole`, or to deliver a Ctrl-C to a long-running job.
|
||||
|
|
|
|||
|
|
@ -101,12 +101,12 @@ def test_wrap_exec_command_describes_the_yield_cap() -> None:
|
|||
wrapped = factory._wrap_exec_command(_running_exec_tool("ok"))
|
||||
|
||||
assert wrapped.description.startswith("Runs a command. The wait for output is capped at 30 s")
|
||||
assert "cannot be read later" in wrapped.description
|
||||
assert "tty=true" in wrapped.description
|
||||
assert 'write_stdin(session_id=<id>, chars="")' in wrapped.description
|
||||
assert "lost" not in wrapped.description
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_wrap_exec_command_flags_a_running_non_tty_process() -> None:
|
||||
async def test_wrap_exec_command_points_a_capped_running_process_at_write_stdin() -> None:
|
||||
wrapped = factory._wrap_exec_command(_running_exec_tool(_RUNNING_RESULT))
|
||||
|
||||
result = await wrapped.on_invoke_tool(
|
||||
|
|
@ -116,20 +116,18 @@ async def test_wrap_exec_command_flags_a_running_non_tty_process() -> None:
|
|||
assert result == (
|
||||
"Chunk ID: abc123\nWall time: 30.0012 seconds\n"
|
||||
"Process running with session ID 7\nOutput:\n"
|
||||
"Note: yield_time_ms=90000 was capped at 30000 (30 s). this non-TTY process cannot be "
|
||||
"polled and its later output is lost. Rerun it in the background "
|
||||
"(`cmd > /tmp/out.log 2>&1 &`, then read the log) or with tty=true.\n"
|
||||
"Note: yield_time_ms=90000 was capped at 30000 (30 s); the command is still running, "
|
||||
'read it with write_stdin(session_id=<id>, chars="").\n'
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_wrap_exec_command_flags_a_running_non_tty_process_within_the_cap() -> None:
|
||||
async def test_wrap_exec_command_leaves_a_running_process_within_the_cap_alone() -> None:
|
||||
wrapped = factory._wrap_exec_command(_running_exec_tool(_RUNNING_RESULT))
|
||||
|
||||
result = await wrapped.on_invoke_tool(cast("Any", None), json.dumps({"cmd": "sleep 60"}))
|
||||
|
||||
assert "Note: this non-TTY process cannot be polled" in result
|
||||
assert "was capped" not in result
|
||||
assert result == _RUNNING_RESULT
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue