Files
wehub-resource-sync 4b6817381b
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
CI / coverage-report (push) Has been cancelled
CI / test-kubernetes (push) Has been cancelled
CI / should-run-thorough (push) Has been cancelled
CI / test-thorough (cloudwatch-demo) (push) Has been cancelled
CI / test-thorough (flink-ecs) (push) Has been cancelled
CI / test-thorough (upstream-lambda) (push) Has been cancelled
CI / test-thorough (prefect-ecs-fargate) (push) Has been cancelled
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Has been cancelled
Benchmark image — build + push to ECR (any adapter) / build + push (push) Has been cancelled
CI / quality (ubuntu-latest) (push) Has been cancelled
CI / test (tools-runtime) (push) Has been cancelled
CI / test (e2e-general) (push) Has been cancelled
CI / test (cli-runtime) (push) Has been cancelled
CI / test (e2e-provider-and-openclaw) (push) Has been cancelled
CI / test (integrations-and-misc) (push) Has been cancelled
Release / verify (push) Has been cancelled
Release / build-python-dist (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Has been cancelled
Release / publish-release (push) Has been cancelled
Release / publish-main-release (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Has been cancelled
Release / prepare (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Has been cancelled
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:10:45 +08:00

480 lines
18 KiB
Python

"""Slash commands for CLI parity, delegating to the Click CLI via subprocess."""
from __future__ import annotations
import contextlib
import json
import os
import shlex
import subprocess
import tempfile
from pathlib import Path
from rich.console import Console
from rich.markup import escape
from core.agent_harness.session.terminal_access import (
session_terminal,
set_turn_outcome_hint,
)
from surfaces.interactive_shell.command_registry.suggestions import closest_choice
from surfaces.interactive_shell.command_registry.types import SlashCommand
from surfaces.interactive_shell.runtime import Session, TaskKind
from surfaces.interactive_shell.runtime.subprocess_runner import (
SYNTHETIC_TEST_TIMEOUT_SECONDS,
build_opensre_cli_argv,
start_background_cli_task,
)
from surfaces.interactive_shell.ui import DIM, ERROR, print_command_output
from surfaces.interactive_shell.utils.telemetry.turn_outcome import format_wizard_cli_outcome
_UPDATE_SUBPROCESS_TIMEOUT_SECONDS = 300
_BACKGROUND_TEST_SUBCOMMANDS = frozenset({"run", "synthetic", "cloudopsbench"})
_TEST_SUBCOMMANDS = ("list", "run", "synthetic", "cloudopsbench")
_TEST_PICKER_SELECTION_FILE_ENV = "OPENSRE_TEST_PICKER_SELECTION_FILE"
_PARENT_INTERACTIVE_SHELL_ENV = "OPENSRE_PARENT_INTERACTIVE_SHELL"
_HEADLESS_CLI_SUBPROCESS_TIMEOUT_SECONDS = 90.0
def publish_headless_slash_response(
session: Session,
*,
message: str,
ok: bool = True,
) -> None:
"""Pin an explicit slash reply for gateway/headless surfaces (wizards, setup)."""
session.complete_latest_record(
"slash",
response_text=message.strip(),
ok=ok,
slash_outcome="headless_guidance",
)
def _decode_subprocess_stream(value: str | bytes | None) -> str:
if value is None:
return ""
if isinstance(value, bytes):
return value.decode("utf-8", errors="replace")
return value
def _cli_command_succeeded(exit_code: int | None) -> bool:
return exit_code == 0
def run_cli_command(
console: Console,
args: list[str],
*,
session: Session | None = None,
subprocess_timeout: float | None = None,
capture_output: bool = False,
) -> bool:
"""Helper to delegate complex or interactive Click commands to a child process.
``subprocess_timeout`` caps how long ``subprocess.run`` waits before raising
:class:`~subprocess.TimeoutExpired`. Interactive flows use ``None`` so the
child can prompt as long as needed; callers that hit the network without a
TTY (like ``opensre update``) pass a bounded timeout.
``capture_output`` (default ``False``) makes the helper capture stdout/stderr
and replay them through ``console`` even without a timeout. Set this for
non-interactive delegated commands (e.g. ``opensre tests list``) so their
output appears inside the REPL buffer instead of bypassing ``console.print``
via the child's inherited stdout FD. Interactive commands like ``onboard``
must leave this ``False`` so the child's prompts stay attached to the real
TTY. Capture is also enabled automatically whenever a timeout is set.
**Return value:** Reports subprocess success for headless/gateway sessions
(``session`` with no terminal facet) so slash analytics can show failure.
On the interactive REPL, always returns ``True`` so delegated CLI failures
do not propagate to :func:`dispatch_slash` and exit the shell — failures
are still recorded via :meth:`Session.mark_latest`.
Ctrl+C sends :exc:`KeyboardInterrupt`, which subclasses :exc:`BaseException`
rather than :exc:`Exception`; it is handled here so the REPL survives and the
child process exits on SIGINT alongside the interrupted ``run`` call.
"""
console.print()
cmd = build_opensre_cli_argv(args)
headless = session is not None and session_terminal(session) is None
should_capture = capture_output or subprocess_timeout is not None or headless
if headless and subprocess_timeout is None:
subprocess_timeout = _HEADLESS_CLI_SUBPROCESS_TIMEOUT_SECONDS
child_env = os.environ.copy()
child_env[_PARENT_INTERACTIVE_SHELL_ENV] = "1"
if should_capture:
# Captured child stdout isn't a TTY, so force Rich colour there and parse
# it back in print_command_output — otherwise its styling would be lost.
child_env["FORCE_COLOR"] = "1"
exit_code: int | None = 0
try:
if should_capture:
captured_result = subprocess.run(
cmd,
check=False,
timeout=subprocess_timeout,
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
env=child_env,
)
exit_code = captured_result.returncode
print_command_output(console, captured_result.stdout or "")
print_command_output(console, captured_result.stderr or "", style=ERROR)
if captured_result.returncode != 0:
console.print(
f"[{ERROR}]CLI command exited with non-zero code {captured_result.returncode}[/]"
)
else:
interactive_result = subprocess.run(cmd, check=False, env=child_env)
exit_code = interactive_result.returncode
if interactive_result.returncode != 0:
console.print(
f"[{ERROR}]CLI command exited with non-zero code {interactive_result.returncode}[/]"
)
except subprocess.TimeoutExpired as exc:
exit_code = None
print_command_output(console, _decode_subprocess_stream(exc.stdout))
print_command_output(console, _decode_subprocess_stream(exc.stderr), style=ERROR)
console.print(f"[{ERROR}]error:[/] CLI command timed out")
except KeyboardInterrupt:
exit_code = None
console.print(f"[{DIM}]CLI command cancelled (Ctrl+C).[/]")
except Exception as exc:
exit_code = None
console.print(f"[{ERROR}]error running CLI command:[/] {exc}")
console.print()
if session is not None and not should_capture:
set_turn_outcome_hint(session, format_wizard_cli_outcome(args, exit_code=exit_code))
ok = _cli_command_succeeded(exit_code)
if session is not None and not ok:
session.mark_latest(ok=False, kind="slash")
# Headless/gateway surfaces need the real exit status for slash analytics.
# Interactive REPL handlers must not return False to dispatch_slash on CLI
# failure — that would exit the shell (/exit is the only intentional False).
return ok if headless else True
def _cmd_onboard(session: Session, console: Console, args: list[str]) -> bool: # noqa: ARG001
if session_terminal(session) is None:
cli_cmd = " ".join(["uv run opensre onboard", *args]).strip()
message = (
"Onboarding is an interactive wizard (LLM provider, integrations, messaging). "
"It cannot run inside a Telegram chat.\n\n"
f"Run on the server:\n {cli_cmd}\n\n"
"Or configure individual services with "
"`/integrations setup <service>`."
)
console.print()
console.print(message)
publish_headless_slash_response(session, message=message)
return True
# The REPL loop treats ``/onboard`` as exclusive-stdin in
# ``runtime.utils.input_policy`` so the prompt_toolkit Application is torn down before
# this handler runs — the wizard subprocess therefore gets exclusive
# stdin and can drive its own interactive prompts without conflicting
# with the shell's UI.
return run_cli_command(console, ["onboard", *args], session=session)
def _cmd_auth(session: Session, console: Console, args: list[str]) -> bool: # noqa: ARG001
capture_output = not args or args[0].lower() in {"status", "logout"}
return run_cli_command(console, ["auth", *args], capture_output=capture_output, session=session)
def _cmd_login(session: Session, console: Console, args: list[str]) -> bool: # noqa: ARG001
return run_cli_command(console, ["auth", "login", *args], session=session)
def _cmd_remote(session: Session, console: Console, args: list[str]) -> bool: # noqa: ARG001
return run_cli_command(console, ["remote", *args], session=session)
def _catalog_task_kind(command: list[str]) -> TaskKind:
return TaskKind.SYNTHETIC_TEST if "synthetic" in command else TaskKind.CLI_COMMAND
def _argv_for_catalog_command(command: list[str]) -> list[str]:
if command[:1] == ["opensre"]:
return build_opensre_cli_argv(command[1:])
return command
def _start_test_command(
*,
session: Session,
console: Console,
command: list[str],
display_command: str | None = None,
) -> None:
shown = display_command or shlex.join(command)
session.record("cli_command", shown)
start_background_cli_task(
display_command=shown,
argv_list=_argv_for_catalog_command(command),
session=session,
console=console,
timeout_seconds=SYNTHETIC_TEST_TIMEOUT_SECONDS,
kind=_catalog_task_kind(command),
use_pty=True,
)
def _run_test_picker_for_background(session: Session, console: Console) -> bool:
console.print()
with contextlib.closing(
tempfile.NamedTemporaryFile(
prefix="opensre-test-selection-",
suffix=".json",
delete=False,
)
) as handle:
selection_path = Path(handle.name)
try:
env = dict(os.environ)
env[_TEST_PICKER_SELECTION_FILE_ENV] = str(selection_path)
result = subprocess.run(
build_opensre_cli_argv(["tests"]),
check=False,
env=env,
)
if result.returncode != 0:
console.print(f"[{ERROR}]CLI command exited with non-zero code {result.returncode}[/]")
console.print()
return True
if not selection_path.stat().st_size:
console.print()
return True
payload = json.loads(selection_path.read_text(encoding="utf-8"))
finally:
with contextlib.suppress(OSError):
selection_path.unlink()
if not isinstance(payload, list):
console.print(f"[{ERROR}]test picker returned an invalid selection[/]")
console.print()
return True
for item in payload:
if not isinstance(item, dict):
continue
command = item.get("command")
if not isinstance(command, list) or not all(isinstance(part, str) for part in command):
continue
display = item.get("command_display")
_start_test_command(
session=session,
console=console,
command=command,
display_command=display if isinstance(display, str) else None,
)
console.print()
return True
def _cmd_tests(session: Session, console: Console, args: list[str]) -> bool:
if not args:
return _run_test_picker_for_background(session, console)
subcommand = args[0].lower()
if subcommand in _BACKGROUND_TEST_SUBCOMMANDS:
_start_test_command(
session=session,
console=console,
command=["opensre", "tests", *args],
)
return True
if subcommand.startswith("-"):
return run_cli_command(console, ["tests", *args], capture_output=True)
if subcommand not in _TEST_SUBCOMMANDS:
suggestion = closest_choice(subcommand, _TEST_SUBCOMMANDS)
if suggestion is None:
console.print(
f"[{ERROR}]unknown tests subcommand:[/] {escape(args[0])} "
"(try [bold]/tests list[/bold], [bold]/tests run <test_id>[/bold], "
"[bold]/tests synthetic[/bold], or [bold]/tests cloudopsbench[/bold])"
)
else:
console.print(
f"[{ERROR}]unknown tests subcommand:[/] {escape(args[0])} "
f"Did you mean [bold]/tests {suggestion}[/bold]?"
)
session.mark_latest(ok=False, kind="slash")
return True
return run_cli_command(console, ["tests", *args], capture_output=True)
def _cmd_guardrails(session: Session, console: Console, args: list[str]) -> bool: # noqa: ARG001
# ``opensre guardrails`` and its subcommands are all non-interactive printers
# (init/test/audit/rules just ``click.echo``). Capture so the output — and
# Click's usage block when no subcommand is given — reaches the REPL buffer
# instead of bypassing ``console.print`` via the child's inherited stdout FD.
return run_cli_command(console, ["guardrails", *args], capture_output=True)
def _cmd_update(session: Session, console: Console, args: list[str]) -> bool: # noqa: ARG001
return run_cli_command(
console,
["update", *args],
subprocess_timeout=_UPDATE_SUBPROCESS_TIMEOUT_SECONDS,
session=session,
)
def _cmd_uninstall(session: Session, console: Console, args: list[str]) -> bool: # noqa: ARG001
return run_cli_command(console, ["uninstall", *args])
def _cmd_config(session: Session, console: Console, args: list[str]) -> bool: # noqa: ARG001
# Non-interactive click.echo only; capture so output reaches the REPL buffer
# instead of the child's inherited stdout while prompt_toolkit redraws.
return run_cli_command(console, ["config", *args], capture_output=True)
def _cmd_messaging(session: Session, console: Console, args: list[str]) -> bool:
# Non-interactive subcommands: capture so output renders through the REPL
# (inherited stdout gets clipped by prompt_toolkit's screen management).
return run_cli_command(console, ["messaging", *args], capture_output=True, session=session)
def _cmd_hermes(session: Session, console: Console, args: list[str]) -> bool: # noqa: ARG001
return run_cli_command(console, ["hermes", *args])
def _cmd_cron(session: Session, console: Console, args: list[str]) -> bool: # noqa: ARG001
return run_cli_command(console, ["cron", *args])
def _cmd_watchdog(session: Session, console: Console, args: list[str]) -> bool: # noqa: ARG001
return run_cli_command(console, ["watchdog", *args])
def _cmd_debug(session: Session, console: Console, args: list[str]) -> bool: # noqa: ARG001
return run_cli_command(console, ["debug", *args])
def _cmd_misses(session: Session, console: Console, args: list[str]) -> bool: # noqa: ARG001
# Non-interactive printers only (list/stats/export/convert) — capture so the
# output reaches the REPL buffer instead of the child's inherited stdout.
return run_cli_command(console, ["misses", *args], capture_output=True)
COMMANDS: list[SlashCommand] = [
SlashCommand(
"/auth",
"Log in to LLM providers and inspect local auth state.",
_cmd_auth,
usage=("/auth", "/auth status", "/auth login deepseek", "/auth logout deepseek"),
),
SlashCommand(
"/login",
"Shortcut for LLM provider login.",
_cmd_login,
usage=("/login", "/login chatgpt", "/login claude", "/login deepseek"),
),
SlashCommand(
"/onboard",
"Run the interactive onboarding wizard.",
_cmd_onboard,
usage=("/onboard", "/onboard local_llm"),
),
SlashCommand(
"/remote",
"Connect to and trigger a remote deployed agent.",
_cmd_remote,
usage=(
"/remote health",
"/remote investigate",
"/remote ops",
"/remote pull",
"/remote trigger",
),
),
SlashCommand(
"/tests",
"Browse and run inventoried tests.",
_cmd_tests,
usage=("/tests", "/tests list", "/tests run", "/tests synthetic"),
first_arg_completions=tuple((name, f"/tests {name}") for name in _TEST_SUBCOMMANDS),
),
SlashCommand(
"/guardrails",
"Manage sensitive information guardrail rules.",
_cmd_guardrails,
usage=(
"/guardrails audit",
"/guardrails init",
"/guardrails rules",
"/guardrails test",
),
),
SlashCommand(
"/update",
"Check for a newer version and update if available.",
_cmd_update,
),
SlashCommand(
"/uninstall",
"Remove OpenSRE and all local data from this machine.",
_cmd_uninstall,
),
SlashCommand(
"/config",
"Show or edit local OpenSRE config.",
_cmd_config,
usage=("/config show", "/config set <key> <value>"),
),
SlashCommand(
"/messaging",
"Manage messaging security and identities.",
_cmd_messaging,
usage=(
"/messaging pair",
"/messaging allow",
"/messaging revoke",
"/messaging status",
),
),
SlashCommand(
"/hermes",
"Live-tail Hermes logs and send incidents to Telegram.",
_cmd_hermes,
usage=("/hermes watch",),
),
SlashCommand(
"/cron",
"Manage cron-driven scheduled deliveries.",
_cmd_cron,
usage=("/cron list", "/cron add", "/cron remove <id>", "/cron run <id>", "/cron logs <id>"),
),
SlashCommand(
"/watchdog",
"Monitor one process and send threshold alarms.",
_cmd_watchdog,
usage=("/watchdog --pid <pid> [--max-rss <size>] [--max-cpu <percent>]",),
examples=("/watchdog --pid 123 --max-rss 1G",),
),
SlashCommand(
"/debug",
"run targeted runtime diagnostics",
_cmd_debug,
),
SlashCommand(
"/misses",
"Triage investigation misses and export them as benchmark scenarios.",
_cmd_misses,
usage=(
"/misses list",
"/misses stats",
"/misses export --out <dir>",
"/misses convert <miss_id>",
),
),
]