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

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",
]