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
166 lines
5.2 KiB
Python
166 lines
5.2 KiB
Python
"""Interaction layer for the REPL execution policy.
|
|
|
|
This module owns the *user-facing* half of the execution gate: it renders the
|
|
policy decision (``Action blocked``, the non-TTY warning, the ``Proceed? [Y/n]``
|
|
prompt), reads the user's confirmation, and emits analytics. The pure decision
|
|
itself is computed by
|
|
:func:`tools.interactive_shell.shared.resolve_confirmation`,
|
|
which has no console, ``input``, or analytics dependency.
|
|
|
|
Keeping interaction here (rather than in ``execution_policy``) means the policy
|
|
module stays pure and easy to test, while callers that need the confirmation UX
|
|
import :func:`execution_allowed` from this UI module.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
from collections.abc import Callable
|
|
from typing import TYPE_CHECKING
|
|
|
|
from rich.console import Console
|
|
from rich.markup import escape
|
|
|
|
from core.agent_harness.session.terminal_access import trust_mode_enabled
|
|
from platform.analytics.cli import capture_repl_execution_policy_decision
|
|
from platform.analytics.provider import Properties
|
|
from platform.terminal.theme import DIM, WARNING
|
|
from tools.interactive_shell.shared import (
|
|
ConfirmationOutcome,
|
|
ExecutionPolicyResult,
|
|
ExecutionVerdict,
|
|
resolve_confirmation,
|
|
)
|
|
|
|
if TYPE_CHECKING:
|
|
from surfaces.interactive_shell.runtime import Session
|
|
|
|
|
|
def _default_confirm_fn(prompt: str) -> str:
|
|
return input(prompt)
|
|
|
|
|
|
DEFAULT_CONFIRM_FN: Callable[[str], str] = _default_confirm_fn
|
|
|
|
|
|
def _emit_decision(
|
|
*,
|
|
tool_type: str,
|
|
policy_verdict: ExecutionVerdict,
|
|
outcome: str,
|
|
trust_mode: bool,
|
|
reason: str | None,
|
|
user_prompted: bool = False,
|
|
) -> None:
|
|
props: Properties = {
|
|
"tool_type": tool_type,
|
|
"policy_verdict": policy_verdict,
|
|
"outcome": outcome,
|
|
"trust_mode": trust_mode,
|
|
}
|
|
if reason:
|
|
props["reason"] = reason[:240]
|
|
if user_prompted:
|
|
props["user_prompted"] = True
|
|
capture_repl_execution_policy_decision(props)
|
|
|
|
|
|
def execution_allowed(
|
|
result: ExecutionPolicyResult,
|
|
*,
|
|
session: Session,
|
|
console: Console,
|
|
action_summary: str,
|
|
confirm_fn: Callable[[str], str] | None = None,
|
|
is_tty: bool | None = None,
|
|
action_already_listed: bool = False,
|
|
) -> bool:
|
|
"""Print policy UX, emit analytics, and return whether execution should proceed.
|
|
|
|
When ``action_already_listed`` is True (e.g. assistant printed a numbered action plan),
|
|
the TTY prompt omits repeating ``action_summary`` and shows only the policy reason.
|
|
"""
|
|
trust_mode = trust_mode_enabled(session)
|
|
tty = sys.stdin.isatty() if is_tty is None else is_tty
|
|
confirm = confirm_fn or DEFAULT_CONFIRM_FN
|
|
|
|
plan = resolve_confirmation(result, trust_mode=trust_mode, is_tty=tty)
|
|
|
|
if plan.outcome == ConfirmationOutcome.DENY:
|
|
_emit_decision(
|
|
tool_type=result.tool_type,
|
|
policy_verdict=result.verdict,
|
|
outcome=plan.analytics_outcome or "blocked",
|
|
trust_mode=trust_mode,
|
|
reason=plan.analytics_reason,
|
|
)
|
|
console.print(f"[{WARNING}]Action blocked:[/] {escape(result.reason or 'not allowed')}")
|
|
if result.hint:
|
|
console.print(f"[{DIM}]{escape(result.hint)}[/]")
|
|
return False
|
|
|
|
if plan.outcome == ConfirmationOutcome.ALLOW:
|
|
_emit_decision(
|
|
tool_type=result.tool_type,
|
|
policy_verdict=result.verdict,
|
|
outcome=plan.analytics_outcome or "allowed",
|
|
trust_mode=trust_mode,
|
|
reason=plan.analytics_reason,
|
|
)
|
|
return True
|
|
|
|
if plan.outcome == ConfirmationOutcome.BLOCK_NON_TTY:
|
|
_emit_decision(
|
|
tool_type=result.tool_type,
|
|
policy_verdict=result.verdict,
|
|
outcome=plan.analytics_outcome or "blocked",
|
|
trust_mode=trust_mode,
|
|
reason=plan.analytics_reason,
|
|
)
|
|
console.print(
|
|
f"[{WARNING}]confirmation required but stdin is not a TTY; "
|
|
f"enable trust mode with[/] [bold]/trust[/bold] [{WARNING}]or rerun in a terminal.[/]"
|
|
)
|
|
console.print(f"[{DIM}]{escape(action_summary)}[/]")
|
|
return False
|
|
|
|
# NEEDS_CONFIRMATION
|
|
reason = (result.reason or "this action").strip()
|
|
summary = action_summary.strip()
|
|
if action_already_listed:
|
|
console.print(f"[{WARNING}]Confirm:[/] [{DIM}]{escape(reason)}[/]")
|
|
elif summary:
|
|
console.print(
|
|
f"[{WARNING}]Confirm[/] [bold]{escape(summary)}[/bold] [{DIM}]— {escape(reason)}[/]"
|
|
)
|
|
else:
|
|
console.print(f"[{WARNING}]Confirm:[/] [{DIM}]{escape(reason)}[/]")
|
|
answer = confirm("Proceed? [Y/n] ").strip().lower()
|
|
if answer not in {"", "y", "yes"}:
|
|
_emit_decision(
|
|
tool_type=result.tool_type,
|
|
policy_verdict=result.verdict,
|
|
outcome="aborted",
|
|
trust_mode=trust_mode,
|
|
reason="user_declined",
|
|
user_prompted=True,
|
|
)
|
|
console.print(f"[{DIM}]cancelled.[/]")
|
|
return False
|
|
|
|
_emit_decision(
|
|
tool_type=result.tool_type,
|
|
policy_verdict=result.verdict,
|
|
outcome="allowed",
|
|
trust_mode=trust_mode,
|
|
reason="user_confirmed",
|
|
user_prompted=True,
|
|
)
|
|
return True
|
|
|
|
|
|
__all__ = [
|
|
"DEFAULT_CONFIRM_FN",
|
|
"execution_allowed",
|
|
]
|