mirror of
https://github.com/hansjone/netx.git
synced 2026-10-09 03:10:46 +08:00
Fix WebCRT command audit loss on merged stdin flush and early input.
Audit every completed stdin line per flush, keep a list of audit_line hints from the router, restore prompt enrichment and stdout fallback, and record actual bytes when hints disagree with what was sent. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
302ebfac23
commit
fcef3b59ac
4 changed files with 260 additions and 35 deletions
|
|
@ -623,6 +623,139 @@ def extract_last_prompt_command(text: str) -> str | None:
|
|||
return None
|
||||
|
||||
|
||||
def _command_tail(line: str) -> str:
|
||||
s = normalize_audit_line(line)
|
||||
for pat in (
|
||||
r"^[\w.-]+(?:\([^)]+\))*[#>]\s*(.*)$",
|
||||
r"^<[^>]+>\s*(.*)$",
|
||||
r"^\[[^\]]+\]\s*(.*)$",
|
||||
):
|
||||
m = re.match(pat, s, flags=re.I)
|
||||
if m:
|
||||
return m.group(1).strip()
|
||||
return s.strip()
|
||||
|
||||
|
||||
def _attach_prompt_prefix(typed: str, hint: str) -> str | None:
|
||||
cmd = str(typed or "").strip()
|
||||
if not cmd:
|
||||
return None
|
||||
h = normalize_audit_line(hint)
|
||||
if not _has_cli_prompt_prefix(h):
|
||||
return None
|
||||
m = re.match(r"^([\w.-]+(?:\([^)]+\))*[#>])\s*", h, flags=re.I)
|
||||
if m:
|
||||
return f"{m.group(1)}{cmd}"
|
||||
m = re.match(r"^(<[^>]+>)\s*", h)
|
||||
if m:
|
||||
return f"{m.group(1)}{cmd}"
|
||||
m = re.match(r"^(\[[^\]]+\])\s*", h)
|
||||
if m:
|
||||
return f"{m.group(1)}{cmd}"
|
||||
return None
|
||||
|
||||
|
||||
def pick_audit_command(
|
||||
stdin_line: str,
|
||||
audit_hint: str | None,
|
||||
*,
|
||||
prompt_hint: str = "",
|
||||
stdout_tail: str = "",
|
||||
source: str = "stdin",
|
||||
) -> str | None:
|
||||
"""Pick auditable text for one completed stdin line (actual send + optional xterm hint)."""
|
||||
typed = normalize_audit_line(stdin_line).strip()
|
||||
hint = normalize_audit_line(audit_hint) if audit_hint else ""
|
||||
src = str(source or "stdin")
|
||||
|
||||
if typed and _is_device_output_line(typed):
|
||||
return None
|
||||
|
||||
if src == "post_login" and typed:
|
||||
return typed
|
||||
|
||||
if src == "early_stdin" and typed and not _is_prompt_only_line(typed):
|
||||
return typed
|
||||
|
||||
if hint and is_auditable_command_line(hint):
|
||||
if not typed:
|
||||
return hint
|
||||
hint_cmd = _command_tail(hint) or hint
|
||||
if typed == hint_cmd or hint_cmd.startswith(typed):
|
||||
return hint
|
||||
# Material disagreement — record bytes actually sent, not xterm hint.
|
||||
return typed if typed else hint
|
||||
|
||||
if typed and is_auditable_command_line(typed):
|
||||
return typed
|
||||
|
||||
for prefix_src in (hint, prompt_hint):
|
||||
enriched = _attach_prompt_prefix(typed, prefix_src)
|
||||
if enriched and is_auditable_command_line(enriched):
|
||||
return enriched
|
||||
|
||||
if stdout_tail:
|
||||
ext = extract_last_prompt_command(stdout_tail)
|
||||
if ext and is_auditable_command_line(ext):
|
||||
ext_cmd = _command_tail(ext) or ext
|
||||
if typed and (typed in ext_cmd or ext_cmd.endswith(typed)):
|
||||
return ext
|
||||
|
||||
if src == "stdin" and typed and not _is_prompt_only_line(typed):
|
||||
return typed
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def resolve_audit_commands(
|
||||
buf_lines: list[str],
|
||||
*,
|
||||
audit_line: str | None = None,
|
||||
audit_lines: list[str] | None = None,
|
||||
prompt_hint: str = "",
|
||||
stdout_tail: str = "",
|
||||
source: str = "stdin",
|
||||
) -> list[str]:
|
||||
"""Map all completed stdin lines in one flush to auditable command strings."""
|
||||
src = str(source or "stdin")
|
||||
if src == "prompt_sync":
|
||||
return []
|
||||
|
||||
if not buf_lines:
|
||||
if audit_line and is_auditable_command_line(normalize_audit_line(audit_line)):
|
||||
return [normalize_audit_line(audit_line)]
|
||||
return []
|
||||
|
||||
hints: list[str | None] = [None] * len(buf_lines)
|
||||
merged: list[str] = [str(x).strip() for x in (audit_lines or []) if str(x).strip()]
|
||||
if audit_line and str(audit_line).strip():
|
||||
if not merged:
|
||||
merged = [str(audit_line).strip()]
|
||||
elif merged[-1] != str(audit_line).strip():
|
||||
merged.append(str(audit_line).strip())
|
||||
|
||||
if merged:
|
||||
if len(merged) == len(buf_lines):
|
||||
hints = list(merged)
|
||||
else:
|
||||
start = max(0, len(buf_lines) - len(merged))
|
||||
for j, h in enumerate(merged):
|
||||
hints[start + j] = h
|
||||
|
||||
out: list[str] = []
|
||||
for i, typed in enumerate(buf_lines):
|
||||
cmd = pick_audit_command(
|
||||
typed,
|
||||
hints[i],
|
||||
prompt_hint=prompt_hint,
|
||||
stdout_tail=stdout_tail if i == len(buf_lines) - 1 else "",
|
||||
source=src,
|
||||
)
|
||||
if cmd:
|
||||
out.append(cmd[:512])
|
||||
return out
|
||||
|
||||
|
||||
def feed_command_line_buffer(buf: str, data: str, *, max_line: int = 512) -> tuple[str, list[str]]:
|
||||
"""Accumulate stdin into completed command lines (Enter / CR / LF).
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue