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
457 lines
14 KiB
Python
457 lines
14 KiB
Python
"""Opensre CLI argv, planning, execution, and presenter-injected runner."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import shlex
|
|
import subprocess
|
|
import sys
|
|
from dataclasses import dataclass
|
|
from enum import StrEnum
|
|
from pathlib import Path
|
|
|
|
from platform.common.task_types import TaskKind
|
|
from tools.interactive_shell.shared import (
|
|
ExecutionPolicyResult,
|
|
ToolExecutionMode,
|
|
ToolExecutionPlan,
|
|
)
|
|
from tools.interactive_shell.subprocess import (
|
|
SHELL_COMMAND_TIMEOUT_SECONDS,
|
|
SubprocessPresenter,
|
|
)
|
|
|
|
# Rich style token names passed to presenter.print_command_output (resolved in surface).
|
|
_ERROR_STYLE = "error"
|
|
|
|
_PYTHON_EXECUTABLE_PREFIXES: tuple[str, ...] = ("python", "pypy")
|
|
|
|
OPENSRE_BLOCKED_SUBCOMMANDS: frozenset[str] = frozenset({"agent"})
|
|
|
|
INTERACTIVE_OPENSRE_COMMAND_PATHS: frozenset[str] = frozenset(
|
|
{
|
|
"onboard",
|
|
"integrations setup",
|
|
}
|
|
)
|
|
|
|
READ_ONLY_OPENSRE_SUBCOMMANDS: frozenset[str] = frozenset(
|
|
{
|
|
"health",
|
|
"version",
|
|
"list",
|
|
"status",
|
|
"show",
|
|
}
|
|
)
|
|
|
|
INVESTIGATION_OPENSRE_SUBCOMMANDS: frozenset[str] = frozenset({"investigate"})
|
|
|
|
|
|
class OpensreCommandClass(StrEnum):
|
|
READ_ONLY = "read_only"
|
|
INVESTIGATION = "investigation"
|
|
MUTATING = "mutating"
|
|
|
|
|
|
class OpensreExecutionMode(StrEnum):
|
|
FOREGROUND = "foreground"
|
|
FOREGROUND_STREAMING = "foreground_streaming"
|
|
BACKGROUND = "background"
|
|
|
|
|
|
class OpensreRunOutcome(StrEnum):
|
|
BLOCKED = "blocked"
|
|
HANDED_OFF = "handed_off"
|
|
EXECUTED_FOREGROUND = "executed_foreground"
|
|
EXECUTED_BACKGROUND = "executed_background"
|
|
DECLINED = "declined"
|
|
INVALID = "invalid"
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class OpensreExecutionPlan:
|
|
classification: OpensreCommandClass
|
|
execution_mode: OpensreExecutionMode
|
|
requires_confirmation: bool
|
|
confirmation_reason: str | None
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class OpensreRunResult:
|
|
outcome: OpensreRunOutcome
|
|
attempted: bool
|
|
display_command: str | None = None
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class ForegroundCliResult:
|
|
"""Outcome of a foreground opensre CLI subprocess."""
|
|
|
|
stdout: str
|
|
stderr: str
|
|
exit_code: int | None
|
|
timed_out: bool
|
|
start_failed: bool
|
|
start_error: str | None = None
|
|
|
|
|
|
def _sys_executable_is_python() -> bool:
|
|
return Path(sys.executable).name.lower().startswith(_PYTHON_EXECUTABLE_PREFIXES)
|
|
|
|
|
|
def _current_opensre_entrypoint() -> str | None:
|
|
"""Return the current ``opensre`` launcher when the REPL was started by one."""
|
|
argv0 = sys.argv[0].strip() if sys.argv else ""
|
|
if not argv0:
|
|
return None
|
|
if Path(argv0).name.lower() not in ("opensre", "opensre.exe"):
|
|
return None
|
|
return argv0
|
|
|
|
|
|
def build_opensre_cli_argv(args: list[str]) -> list[str]:
|
|
"""Return argv for re-entering the OpenSRE Click CLI."""
|
|
if entrypoint := _current_opensre_entrypoint():
|
|
return [entrypoint, *args]
|
|
if getattr(sys, "frozen", False) or not _sys_executable_is_python():
|
|
return [sys.executable, *args]
|
|
return [sys.executable, "-m", "surfaces.cli", *args]
|
|
|
|
|
|
def is_interactive_wizard(tokens: list[str]) -> bool:
|
|
"""True when ``tokens`` name an opensre subcommand that needs a full TTY wizard."""
|
|
if not tokens:
|
|
return False
|
|
one = tokens[0].lower()
|
|
if one in INTERACTIVE_OPENSRE_COMMAND_PATHS:
|
|
return True
|
|
if len(tokens) < 2:
|
|
return False
|
|
two = f"{one} {tokens[1].lower()}"
|
|
return two in INTERACTIVE_OPENSRE_COMMAND_PATHS
|
|
|
|
|
|
def interactive_wizard_handoff_response_text(command_str: str) -> str:
|
|
"""Plain-text outcome for analytics when a wizard is redirected to a slash command."""
|
|
return (
|
|
f"`opensre {command_str}` is an interactive wizard that needs a full terminal. "
|
|
f"Type /{command_str} directly in this shell to launch it."
|
|
)
|
|
|
|
|
|
def classify_opensre_command(tokens: list[str]) -> str:
|
|
first_token = tokens[0].lower()
|
|
if first_token in READ_ONLY_OPENSRE_SUBCOMMANDS:
|
|
return OpensreCommandClass.READ_ONLY.value
|
|
if first_token in INVESTIGATION_OPENSRE_SUBCOMMANDS:
|
|
return OpensreCommandClass.INVESTIGATION.value
|
|
if first_token == "fleet":
|
|
subcommand = tokens[1].lower() if len(tokens) > 1 else "list"
|
|
if subcommand in {"list"}:
|
|
return OpensreCommandClass.READ_ONLY.value
|
|
if subcommand == "scan" and "--register" not in tokens[2:]:
|
|
return OpensreCommandClass.READ_ONLY.value
|
|
return OpensreCommandClass.MUTATING.value
|
|
|
|
|
|
def opensre_confirmation_reason(tokens: list[str]) -> str:
|
|
if tokens[:2] == ["fleet", "scan"] and "--register" in tokens[2:]:
|
|
return "register discovered local AI-agent processes"
|
|
if tokens and tokens[0] == "fleet":
|
|
return "this updates the local AI-agent registry"
|
|
return "this opensre subcommand may change local config or infrastructure"
|
|
|
|
|
|
def build_opensre_execution_plan(tokens: list[str]) -> OpensreExecutionPlan:
|
|
"""Compute classification + execution mode from one canonical policy table."""
|
|
classification = OpensreCommandClass(classify_opensre_command(tokens))
|
|
first_token = tokens[0].lower()
|
|
|
|
execution_mode = OpensreExecutionMode.BACKGROUND
|
|
if first_token in READ_ONLY_OPENSRE_SUBCOMMANDS:
|
|
execution_mode = OpensreExecutionMode.FOREGROUND
|
|
elif first_token == "fleet":
|
|
subcommand = tokens[1].lower() if len(tokens) > 1 else "list"
|
|
if subcommand == "watch":
|
|
execution_mode = OpensreExecutionMode.FOREGROUND_STREAMING
|
|
elif subcommand in {"list", "register", "forget", "scan"}:
|
|
execution_mode = OpensreExecutionMode.FOREGROUND
|
|
|
|
requires_confirmation = classification is OpensreCommandClass.MUTATING
|
|
reason = (
|
|
opensre_confirmation_reason([token.lower() for token in tokens])
|
|
if requires_confirmation
|
|
else None
|
|
)
|
|
return OpensreExecutionPlan(
|
|
classification=classification,
|
|
execution_mode=execution_mode,
|
|
requires_confirmation=requires_confirmation,
|
|
confirmation_reason=reason,
|
|
)
|
|
|
|
|
|
def to_tool_execution_plan(plan: OpensreExecutionPlan) -> ToolExecutionPlan:
|
|
mode = ToolExecutionMode.BACKGROUND
|
|
if plan.execution_mode is OpensreExecutionMode.FOREGROUND:
|
|
mode = ToolExecutionMode.FOREGROUND
|
|
elif plan.execution_mode is OpensreExecutionMode.FOREGROUND_STREAMING:
|
|
mode = ToolExecutionMode.FOREGROUND_STREAMING
|
|
if not plan.requires_confirmation:
|
|
policy = ExecutionPolicyResult(
|
|
verdict="allow",
|
|
tool_type="cli_command",
|
|
reason=None,
|
|
hint=None,
|
|
shell_classification=plan.classification.value,
|
|
)
|
|
else:
|
|
policy = ExecutionPolicyResult(
|
|
verdict="ask",
|
|
tool_type="cli_command",
|
|
reason=plan.confirmation_reason,
|
|
hint="Use a read-only subcommand (health, version, list, status, show)",
|
|
shell_classification=plan.classification.value,
|
|
)
|
|
return ToolExecutionPlan(
|
|
tool_type="cli_command",
|
|
classification=plan.classification.value,
|
|
execution_mode=mode,
|
|
policy=policy,
|
|
)
|
|
|
|
|
|
def run_foreground_cli(
|
|
argv_list: list[str],
|
|
*,
|
|
timeout_seconds: int = SHELL_COMMAND_TIMEOUT_SECONDS,
|
|
) -> ForegroundCliResult:
|
|
try:
|
|
completed = subprocess.run(
|
|
argv_list,
|
|
capture_output=True,
|
|
text=True,
|
|
encoding="utf-8",
|
|
errors="replace",
|
|
timeout=timeout_seconds,
|
|
check=False,
|
|
)
|
|
except subprocess.TimeoutExpired as exc:
|
|
return ForegroundCliResult(
|
|
stdout=str(exc.output or ""),
|
|
stderr=str(exc.stderr or ""),
|
|
exit_code=None,
|
|
timed_out=True,
|
|
start_failed=False,
|
|
)
|
|
except Exception as exc: # noqa: BLE001
|
|
return ForegroundCliResult(
|
|
stdout="",
|
|
stderr="",
|
|
exit_code=None,
|
|
timed_out=False,
|
|
start_failed=True,
|
|
start_error=str(exc),
|
|
)
|
|
return ForegroundCliResult(
|
|
stdout=completed.stdout or "",
|
|
stderr=completed.stderr or "",
|
|
exit_code=completed.returncode,
|
|
timed_out=False,
|
|
start_failed=False,
|
|
)
|
|
|
|
|
|
def spawn_streaming_cli(argv_list: list[str]) -> subprocess.Popen[str]:
|
|
return subprocess.Popen(
|
|
argv_list,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.STDOUT,
|
|
text=True,
|
|
encoding="utf-8",
|
|
errors="replace",
|
|
)
|
|
|
|
|
|
def _print_wizard_handoff(presenter: SubprocessPresenter, command_str: str) -> None:
|
|
presenter.print(
|
|
f"[warning]`opensre {command_str}` is an interactive wizard that needs a full terminal.[/]"
|
|
)
|
|
presenter.print(
|
|
f"[dim]Type [bold]/{command_str}[/bold] directly in this shell to launch it.[/]"
|
|
)
|
|
|
|
|
|
def _run_foreground_via_presenter(
|
|
presenter: SubprocessPresenter,
|
|
*,
|
|
argv_list: list[str],
|
|
display_command: str,
|
|
) -> None:
|
|
presenter.print_bold_command(display_command)
|
|
result = run_foreground_cli(argv_list, timeout_seconds=SHELL_COMMAND_TIMEOUT_SECONDS)
|
|
if result.start_failed:
|
|
if result.start_error:
|
|
presenter.report_exception(
|
|
RuntimeError(result.start_error),
|
|
context="surfaces.interactive_shell.opensre_cli.start",
|
|
)
|
|
presenter.print_error(f"failed to start: {result.start_error}")
|
|
presenter.session.record("cli_command", display_command, ok=False)
|
|
return
|
|
presenter.print_command_output(result.stdout)
|
|
presenter.print_command_output(result.stderr, style=_ERROR_STYLE)
|
|
if result.timed_out:
|
|
presenter.print(
|
|
f"[error]command timed out after {SHELL_COMMAND_TIMEOUT_SECONDS} seconds[/]"
|
|
)
|
|
presenter.session.record("cli_command", display_command, ok=False)
|
|
return
|
|
ok = result.exit_code == 0
|
|
if not ok:
|
|
presenter.print(f"[error]command failed (exit {result.exit_code}):[/]")
|
|
presenter.session.record("cli_command", display_command, ok=ok)
|
|
|
|
|
|
def _run_streaming_via_presenter(
|
|
presenter: SubprocessPresenter,
|
|
*,
|
|
argv_list: list[str],
|
|
display_command: str,
|
|
) -> None:
|
|
presenter.print_bold_command(display_command)
|
|
try:
|
|
proc = spawn_streaming_cli(argv_list)
|
|
except Exception as exc: # noqa: BLE001
|
|
presenter.report_exception(
|
|
exc,
|
|
context="surfaces.interactive_shell.opensre_cli.start",
|
|
)
|
|
presenter.print_error(f"failed to start: {exc}")
|
|
presenter.session.record("cli_command", display_command, ok=False)
|
|
return
|
|
if proc.stdout is not None:
|
|
for line in proc.stdout:
|
|
presenter.print_command_output(line)
|
|
code = proc.wait()
|
|
ok = code == 0
|
|
if not ok:
|
|
presenter.print(f"[error]command failed (exit {code}):[/]")
|
|
presenter.session.record("cli_command", display_command, ok=ok)
|
|
|
|
|
|
def run_opensre_cli_command_result(
|
|
args: str,
|
|
presenter: SubprocessPresenter,
|
|
) -> OpensreRunResult:
|
|
"""Run an opensre subcommand (not agent) via the injected presenter."""
|
|
try:
|
|
tokens = shlex.split(args)
|
|
except ValueError:
|
|
tokens = args.split()
|
|
if not tokens:
|
|
return OpensreRunResult(outcome=OpensreRunOutcome.INVALID, attempted=False)
|
|
|
|
first_token = tokens[0].lower()
|
|
if first_token in OPENSRE_BLOCKED_SUBCOMMANDS:
|
|
presenter.print(f"[error]Cannot run `opensre {first_token}`: subcommand is blocked.[/]")
|
|
return OpensreRunResult(outcome=OpensreRunOutcome.BLOCKED, attempted=False)
|
|
|
|
if is_interactive_wizard(tokens):
|
|
command_str = " ".join(tokens)
|
|
_print_wizard_handoff(presenter, command_str)
|
|
presenter.session.record(
|
|
"cli_command",
|
|
f"opensre {command_str}",
|
|
ok=False,
|
|
response_text=interactive_wizard_handoff_response_text(command_str),
|
|
)
|
|
return OpensreRunResult(
|
|
outcome=OpensreRunOutcome.HANDED_OFF,
|
|
attempted=True,
|
|
display_command=f"opensre {command_str}",
|
|
)
|
|
|
|
plan = build_opensre_execution_plan(tokens)
|
|
execution_plan = to_tool_execution_plan(plan)
|
|
display_command = f"opensre {' '.join(tokens)}"
|
|
|
|
if not presenter.execution_allowed(
|
|
execution_plan.policy,
|
|
action_summary=f"$ {display_command}",
|
|
):
|
|
presenter.session.record("cli_command", display_command, ok=False)
|
|
return OpensreRunResult(
|
|
outcome=OpensreRunOutcome.DECLINED,
|
|
attempted=True,
|
|
display_command=display_command,
|
|
)
|
|
|
|
argv_list = build_opensre_cli_argv(tokens)
|
|
if execution_plan.execution_mode in {
|
|
ToolExecutionMode.FOREGROUND,
|
|
ToolExecutionMode.FOREGROUND_STREAMING,
|
|
}:
|
|
if execution_plan.execution_mode is ToolExecutionMode.FOREGROUND_STREAMING:
|
|
_run_streaming_via_presenter(
|
|
presenter,
|
|
argv_list=argv_list,
|
|
display_command=display_command,
|
|
)
|
|
else:
|
|
_run_foreground_via_presenter(
|
|
presenter,
|
|
argv_list=argv_list,
|
|
display_command=display_command,
|
|
)
|
|
return OpensreRunResult(
|
|
outcome=OpensreRunOutcome.EXECUTED_FOREGROUND,
|
|
attempted=True,
|
|
display_command=display_command,
|
|
)
|
|
|
|
presenter.session.record("cli_command", display_command)
|
|
presenter.start_background_cli_task(
|
|
display_command=display_command,
|
|
argv_list=argv_list,
|
|
timeout_seconds=SHELL_COMMAND_TIMEOUT_SECONDS,
|
|
kind=TaskKind.CLI_COMMAND,
|
|
)
|
|
return OpensreRunResult(
|
|
outcome=OpensreRunOutcome.EXECUTED_BACKGROUND,
|
|
attempted=True,
|
|
display_command=display_command,
|
|
)
|
|
|
|
|
|
def run_opensre_cli_command(args: str, presenter: SubprocessPresenter) -> bool:
|
|
result = run_opensre_cli_command_result(args, presenter)
|
|
return result.attempted
|
|
|
|
|
|
__all__ = [
|
|
"ForegroundCliResult",
|
|
"INTERACTIVE_OPENSRE_COMMAND_PATHS",
|
|
"INVESTIGATION_OPENSRE_SUBCOMMANDS",
|
|
"OPENSRE_BLOCKED_SUBCOMMANDS",
|
|
"READ_ONLY_OPENSRE_SUBCOMMANDS",
|
|
"OpensreCommandClass",
|
|
"OpensreExecutionMode",
|
|
"OpensreExecutionPlan",
|
|
"OpensreRunOutcome",
|
|
"OpensreRunResult",
|
|
"_run_foreground_via_presenter",
|
|
"_run_streaming_via_presenter",
|
|
"build_opensre_cli_argv",
|
|
"build_opensre_execution_plan",
|
|
"classify_opensre_command",
|
|
"interactive_wizard_handoff_response_text",
|
|
"is_interactive_wizard",
|
|
"opensre_confirmation_reason",
|
|
"run_foreground_cli",
|
|
"run_opensre_cli_command",
|
|
"run_opensre_cli_command_result",
|
|
"spawn_streaming_cli",
|
|
"to_tool_execution_plan",
|
|
]
|