blob: 82d8466b39315dcdf9db4eb93cc4ff69990677ef [file]
// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/actor/execution_engine.h"
#include <algorithm>
#include <cstddef>
#include <memory>
#include <optional>
#include <utility>
#include "base/check.h"
#include "base/check_deref.h"
#include "base/check_is_test.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/logging.h"
#include "base/memory/weak_ptr.h"
#include "base/metrics/histogram_functions.h"
#include "base/no_destructor.h"
#include "base/notimplemented.h"
#include "base/state_transitions.h"
#include "base/time/time.h"
#include "base/trace_event/trace_event.h"
#include "base/types/expected.h"
#include "base/types/id_type.h"
#include "base/types/optional_ref.h"
#include "base/types/pass_key.h"
#include "chrome/browser/actor/action_tracker_for_metrics.h"
#include "chrome/browser/actor/actor_features.h"
#include "chrome/browser/actor/actor_keyed_service.h"
#include "chrome/browser/actor/actor_metrics.h"
#include "chrome/browser/actor/actor_proto_conversion.h"
#include "chrome/browser/actor/actor_task.h"
#include "chrome/browser/actor/actor_util.h"
#include "chrome/browser/actor/enterprise_policy_url_checker.h"
#include "chrome/browser/actor/origin_checker.h"
#include "chrome/browser/actor/safety_list_manager.h"
#include "chrome/browser/actor/site_policy.h"
#include "chrome/browser/actor/tools/attempt_login_tool.h"
#include "chrome/browser/actor/tools/navigate_tool_request.h"
#include "chrome/browser/actor/tools/tool_controller.h"
#include "chrome/browser/actor/tools/tool_request.h"
#include "chrome/browser/actor/ui/event_dispatcher.h"
#include "chrome/browser/affiliations/affiliation_service_factory.h"
#include "chrome/browser/autofill/actor/actor_form_filling_service_impl.h"
#include "chrome/browser/favicon/favicon_service_factory.h"
#include "chrome/browser/password_manager/actor_login/actor_login_service.h"
#include "chrome/browser/password_manager/actor_login/actor_login_service_impl.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/common/actor.mojom.h"
#include "chrome/common/actor/action_result.h"
#include "chrome/common/actor/journal_details_builder.h"
#include "chrome/common/actor/task_id.h"
#include "chrome/common/chrome_features.h"
#include "components/affiliations/core/browser/affiliation_service.h"
#include "components/keyed_service/core/service_access_type.h"
#include "components/optimization_guide/content/browser/page_content_proto_provider.h"
#include "components/optimization_guide/proto/features/actions_data.pb.h"
#include "components/password_manager/core/browser/features/password_features.h"
#include "components/tabs/public/tab_interface.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/navigation_throttle.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_contents_observer.h"
#include "mojo/public/cpp/base/proto_wrapper.h"
#include "services/metrics/public/cpp/ukm_builders.h"
#include "third_party/abseil-cpp/absl/container/flat_hash_set.h"
#include "third_party/abseil-cpp/absl/strings/str_format.h"
#include "ui/event_dispatcher.h"
#include "url/origin.h"
using content::RenderFrameHost;
using content::WebContents;
using optimization_guide::DocumentIdentifierUserData;
using optimization_guide::proto::Action;
using optimization_guide::proto::Actions;
using optimization_guide::proto::ActionTarget;
using optimization_guide::proto::AnnotatedPageContent;
using tabs::TabInterface;
namespace actor {
namespace {
static constexpr std::string_view kPermissionGrantedHistogram =
"Actor.NavigationGating.PermissionGranted";
static constexpr std::string_view kActionNavigationsApprovedByServerHistogram =
"Actor.NavigationGating.ActionNavigationsApprovedByServer";
BASE_FEATURE(kActorReloadCrashedTabBeforeAct, base::FEATURE_ENABLED_BY_DEFAULT);
RenderFrameHost* GetPrimaryMainFrame(
content::NavigationHandle& navigation_handle) {
return navigation_handle.GetWebContents()->GetPrimaryMainFrame();
}
void PostTaskForActCallback(
ActorTask::ActCallback callback,
std::vector<ActionResultWithLatencyInfo> action_results) {
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
base::BindOnce(std::move(callback), std::move(action_results)));
}
// When operating on an opaque site, we choose to use the precursor's origin
// when judging whether a user confirmation should be triggered or not. We are
// effictively, using `rfh.GetLastCommittedUrl()` vs
// `rfh.GetLastCommittedOrigin()` for this "security" purpose contrary to the
// guidance here (docs/security/origin-vs-url.md).
//
// This is an intentional decision since it relates to user confirmations and it
// would be confusing to ask the user to distinguish between opaque domains.
url::Origin OriginOrPrecursorIfOpaque(const url::Origin& origin) {
if (!origin.opaque()) {
return origin;
}
return url::Origin::Create(
origin.GetTupleOrPrecursorTupleIfOpaque().GetURL());
}
} // namespace
ToolDelegate::CredentialWithPermission::CredentialWithPermission() = default;
ToolDelegate::CredentialWithPermission::CredentialWithPermission(
const actor_login::Credential& credential,
webui::mojom::UserGrantedPermissionDuration permission_duration)
: credential(credential), permission_duration(permission_duration) {}
ToolDelegate::CredentialWithPermission::CredentialWithPermission(
const CredentialWithPermission&) = default;
ToolDelegate::CredentialWithPermission::CredentialWithPermission(
CredentialWithPermission&&) = default;
ToolDelegate::CredentialWithPermission&
ToolDelegate::CredentialWithPermission::operator=(
const CredentialWithPermission&) = default;
ToolDelegate::CredentialWithPermission&
ToolDelegate::CredentialWithPermission::operator=(CredentialWithPermission&&) =
default;
ToolDelegate::CredentialWithPermission::~CredentialWithPermission() = default;
// static
ExecutionEngine::FactoryFunction&
ExecutionEngine::GetFactoryFunctionForTesting() {
static base::NoDestructor<FactoryFunction> callback;
return *callback;
}
ExecutionEngine::ExecutionEngine(base::PassKey<ExecutionEngine> pass_key,
ActorTask& owner_task)
: ExecutionEngine(
pass_key,
owner_task,
ui::NewUiEventDispatcher(
owner_task.actor_keyed_service().GetActorUiStateManager())) {}
// Protected constructor without pass key to allow subclassing.
ExecutionEngine::ExecutionEngine(ActorTask& owner_task)
: ExecutionEngine(base::PassKey<ExecutionEngine>(), owner_task) {}
ExecutionEngine::ExecutionEngine(
base::PassKey<ExecutionEngine>,
ActorTask& owner_task,
std::unique_ptr<ui::UiEventDispatcher> ui_event_dispatcher)
: task_(owner_task),
journal_(task_->actor_keyed_service().GetJournal().GetSafeRef()),
tool_controller_(std::make_unique<ToolController>(*task_, *this)),
actor_login_service_(
std::make_unique<actor_login::ActorLoginServiceImpl>()),
actor_form_filling_service_(
std::make_unique<autofill::ActorFormFillingServiceImpl>()),
ui_event_dispatcher_(std::move(ui_event_dispatcher)) {
TRACE_EVENT0("actor", "ExecutionEngine::ExecutionEngine");
}
// static
std::unique_ptr<ExecutionEngine> ExecutionEngine::Create(
ActorTask& owner_task) {
if (!GetFactoryFunctionForTesting().is_null()) {
return GetFactoryFunctionForTesting().Run(owner_task);
}
return std::make_unique<ExecutionEngine>(base::PassKey<ExecutionEngine>(),
owner_task);
}
std::unique_ptr<ExecutionEngine> ExecutionEngine::CreateForTesting(
ActorTask& owner_task,
std::unique_ptr<ui::UiEventDispatcher> ui_event_dispatcher) {
return std::make_unique<ExecutionEngine>(base::PassKey<ExecutionEngine>(),
owner_task,
std::move(ui_event_dispatcher));
}
ExecutionEngine::~ExecutionEngine() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
origin_checker_.RecordSizeMetrics();
RunUserTakeoverCallbackIfExists(/*should_cancel=*/true);
}
void ExecutionEngine::SetState(State state) {
TRACE_EVENT0("actor", "ExecutionEngine::SetState");
journal_->Log(GURL(), task_->id(), "ExecutionEngine::StateChange",
JournalDetailsBuilder()
.Add("current_state", StateToString(state_))
.Add("new_state", StateToString(state))
.Build());
#if DCHECK_IS_ON()
static const base::NoDestructor<base::StateTransitions<State>> transitions(
base::StateTransitions<State>({
{State::kInit, {State::kStartAction, State::kComplete}},
{State::kStartAction,
{State::kToolCreateAndVerify, State::kComplete}},
{State::kToolCreateAndVerify,
{State::kUiPreInvoke, State::kComplete}},
{State::kUiPreInvoke, {State::kToolInvoke, State::kComplete}},
{State::kToolInvoke, {State::kUiPostInvoke, State::kComplete}},
{State::kUiPostInvoke, {State::kComplete, State::kStartAction}},
{State::kComplete, {State::kStartAction}},
}));
DCHECK_STATE_TRANSITION(transitions, state_, state);
#endif // DCHECK_IS_ON()
observers_.Notify(&StateObserver::OnStateChanged, state_, state);
state_ = state;
}
std::string ExecutionEngine::StateToString(State state) {
switch (state) {
case State::kInit:
return "INIT";
case State::kStartAction:
return "START_ACTION";
case State::kToolCreateAndVerify:
return "CREATE_AND_VERIFY";
case State::kUiPreInvoke:
return "UI_PRE_INVOKE";
case State::kToolInvoke:
return "TOOL_INVOKE";
case State::kUiPostInvoke:
return "UI_POST_INVOKE";
case State::kComplete:
return "COMPLETE";
}
}
content::NavigationThrottle::ThrottleAction
ExecutionEngine::ShouldDeferNavigation(
content::NavigationHandle& navigation_handle,
ExecutionEngine::NavigationDecisionCallback callback) {
if (!IsNavigationGatingEnabled()) {
return content::NavigationThrottle::PROCEED;
}
CHECK(navigation_handle.GetNavigatingFrameType() ==
content::FrameType::kPrimaryMainFrame ||
navigation_handle.GetNavigatingFrameType() ==
content::FrameType::kPrerenderMainFrame);
CHECK(!navigation_handle.HasCommitted());
base::ScopedUmaHistogramTimer timer(
"Actor.NavigationGating.TimeElapsedForGating2");
// Note: `DetermineGatingDecision` operates on GURLs, but `origin_checker_`
// operates on Origins only.
const GURL& source_url =
GetPrimaryMainFrame(navigation_handle)->GetLastCommittedURL();
const url::Origin source_origin = url::Origin::Create(source_url);
const GatingDecision decision =
DetermineGatingDecision(source_url,
/*destination_url=*/navigation_handle.GetURL());
RecordNavigationGatingDecision(decision);
switch (decision) {
case GatingDecision::kAllowSameOrigin:
LogNavigationGating(source_origin, navigation_handle.GetInitiatorOrigin(),
url::Origin::Create(navigation_handle.GetURL()),
/*applied_gate=*/false);
MaybeRecordNavigationConfirmationMetrics(
state(), url::Origin::Create(navigation_handle.GetURL()),
/*is_pre_approved=*/true);
return content::NavigationThrottle::PROCEED;
case GatingDecision::kAllowByStaticList:
LogNavigationGating(source_origin, navigation_handle.GetInitiatorOrigin(),
url::Origin::Create(navigation_handle.GetURL()),
/*applied_gate=*/false);
MaybeRecordNavigationConfirmationMetrics(
state(), url::Origin::Create(navigation_handle.GetURL()),
/*is_pre_approved=*/false);
return content::NavigationThrottle::PROCEED;
case GatingDecision::kBlockByStaticList:
LogNavigationGating(source_origin, navigation_handle.GetInitiatorOrigin(),
url::Origin::Create(navigation_handle.GetURL()),
/*applied_gate=*/true);
return content::NavigationThrottle::CANCEL_AND_IGNORE;
case GatingDecision::kNeedsAsyncCheck: {
bool skip_prompt = navigation_handle.IsInPrerenderedMainFrame();
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
base::BindOnce(
&ExecutionEngine::CheckNavigationSensitiveUrlList, GetWeakPtr(),
source_origin, navigation_handle.GetInitiatorOrigin(),
navigation_handle.GetURL(),
GetPrimaryMainFrame(navigation_handle)->GetPageUkmSourceId(),
skip_prompt, std::move(timer),
std::move(callback).Then(base::BindOnce(
&ExecutionEngine::MaybeRecordNavigationConfirmationMetrics,
GetWeakPtr(), state(),
url::Origin::Create(navigation_handle.GetURL()),
/*is_pre_approved=*/false))));
return content::NavigationThrottle::DEFER;
}
}
NOTREACHED();
}
void ExecutionEngine::LogNavigationGating(
const url::Origin& source,
base::optional_ref<const url::Origin> initiator,
const url::Origin& destination,
bool applied_gate) const {
base::UmaHistogramBoolean("Actor.NavigationGating.AppliedGate", applied_gate);
base::UmaHistogramBoolean("Actor.NavigationGating.SameOriginSource",
source.IsSameOriginWith(destination));
base::UmaHistogramBoolean(
"Actor.NavigationGating.SameSiteSource",
net::SchemefulSite::IsSameSite(source, destination));
if (initiator) {
base::UmaHistogramBoolean("Actor.NavigationGating.SameOriginInitiator",
initiator->IsSameOriginWith(destination));
base::UmaHistogramBoolean(
"Actor.NavigationGating.SameSiteInitiator",
net::SchemefulSite::IsSameSite(*initiator, destination));
}
}
ExecutionEngine::GatingDecision ExecutionEngine::DetermineGatingDecision(
const GURL& source_url,
const GURL& destination_url) const {
switch (task_->policy_checker().Evaluate(destination_url)) {
case EnterprisePolicyBlockReason::kNotBlocked:
break;
case EnterprisePolicyBlockReason::kExplicitlyAllowed:
return GatingDecision::kAllowByStaticList;
case EnterprisePolicyBlockReason::kExplicitlyBlocked:
return GatingDecision::kBlockByStaticList;
}
switch (SafetyListManager::GetInstance()->Find(source_url, destination_url)) {
case SafetyListManager::Decision::kNone:
return url::IsSameOriginWith(source_url, destination_url)
? GatingDecision::kAllowSameOrigin
: GatingDecision::kNeedsAsyncCheck;
case SafetyListManager::Decision::kAllow:
return GatingDecision::kAllowByStaticList;
case SafetyListManager::Decision::kBlock:
return GatingDecision::kBlockByStaticList;
}
NOTREACHED();
}
void ExecutionEngine::CheckNavigationSensitiveUrlList(
const url::Origin& source,
const std::optional<url::Origin>& initiator,
const GURL& destination_url,
ukm::SourceId ukm_source_id,
bool skip_prompt,
base::ScopedUmaHistogramTimer timer,
ExecutionEngine::NavigationDecisionCallback callback) {
url::Origin destination_origin = url::Origin::Create(destination_url);
// Check previously confirmed origins. If the user has previously confirmed
// the origin is allowed, we should proceed and not double prompt.
if (origin_checker_.IsNavigationConfirmedByUser(destination_origin)) {
OnNavigationSensitiveUrlListChecked(source, initiator, destination_origin,
ukm_source_id, skip_prompt,
std::move(timer), std::move(callback),
/*not_sensitive=*/true);
return;
}
base::expected<void, DecisionCallback> sensitive_check_result =
MaybeCheckOptimizationGuideForSensitiveUrl(
destination_url, task_->GetProfile(),
base::BindOnce(&ExecutionEngine::OnNavigationSensitiveUrlListChecked,
GetWeakPtr(), source, initiator, destination_origin,
ukm_source_id, skip_prompt, std::move(timer),
std::move(callback)));
if (!sensitive_check_result.has_value()) {
std::move(sensitive_check_result).error().Run(/*not_sensitive=*/true);
}
}
void ExecutionEngine::OnNavigationSensitiveUrlListChecked(
const url::Origin& source,
const std::optional<url::Origin>& initiator,
const url::Origin& destination,
ukm::SourceId ukm_source_id,
bool skip_prompt,
base::ScopedUmaHistogramTimer timer,
ExecutionEngine::NavigationDecisionCallback callback,
bool not_sensitive) {
// If not sensitive, check if it's an origin the actor has previously
// interacted with or received instructions from the server to interact with.
if (not_sensitive &&
origin_checker_.IsNavigationAllowed(initiator, destination)) {
LogNavigationGating(source, initiator, destination,
/*applied_gate=*/false);
ukm::builders::Actor_OriginGating builder(ukm_source_id);
builder
.SetServerConfirmationResult(static_cast<int64_t>(
ExecutionEngine::ActorServerConfirmationResult::kNotRequired))
.SetEngineState(static_cast<int64_t>(state_));
builder.Record(ukm::UkmRecorder::Get());
std::move(callback).Run(/*may_continue=*/true);
return;
}
// At this point, the navigation is either blocked OR not on the allowlist.
LogNavigationGating(source, initiator, destination,
/*applied_gate=*/true);
if (skip_prompt) {
std::move(callback).Run(/*may_continue=*/false);
return;
}
// If the origin is not sensitive *and* not already allowed, this is a novel
// origin and we should either confirm the navigation with the web client or
// prompt the user depending on the feature state.
if (not_sensitive) {
HandleNavigationToNewOrigin(destination, ukm_source_id, std::move(timer),
std::move(callback));
return;
}
// If we cannot prompt for sensitive navigations, then we block instead.
if (!kGlicPromptUserForSensitiveNavigations.Get()) {
std::move(callback).Run(/*may_continue=*/false);
return;
}
// Otherwise, present a user confirmation dialog to continue.
SendUserConfirmationDialogRequest(destination,
/*for_sensitive_origin=*/true,
std::move(timer), std::move(callback));
}
void ExecutionEngine::HandleNavigationToNewOrigin(
const url::Origin& destination,
ukm::SourceId ukm_source_id,
base::ScopedUmaHistogramTimer timer,
ExecutionEngine::NavigationDecisionCallback callback) {
if (!kGlicConfirmNavigationToNewOrigins.Get()) {
std::move(callback).Run(/*may_continue=*/true);
return;
}
if (kGlicPromptUserForNavigationToNewOrigins.Get()) {
SendUserConfirmationDialogRequest(destination,
/*for_sensitive_origin=*/false,
std::move(timer), std::move(callback));
return;
}
SendNavigationConfirmationRequest(destination, ukm_source_id,
std::move(timer), std::move(callback));
}
void ExecutionEngine::SendNavigationConfirmationRequest(
const url::Origin& destination,
ukm::SourceId ukm_source_id,
base::ScopedUmaHistogramTimer timer,
ExecutionEngine::NavigationDecisionCallback callback) {
if (!task_->delegate()) {
std::move(callback).Run(/*may_continue=*/false);
return;
}
task_->delegate()->RequestToConfirmNavigation(
task_->id(), destination,
base::BindOnce(&ExecutionEngine::OnNavigationConfirmationDecision,
GetWeakPtr(), destination, ukm_source_id, std::move(timer),
std::move(callback)));
}
void ExecutionEngine::MaybeRecordNavigationConfirmationMetrics(
ExecutionEngine::State state_for_metrics,
const url::Origin& destination,
bool is_pre_approved) {
if (!base::FeatureList::IsEnabled(
kGlicRecordNavigationConfirmationRequestMetrics)) {
return;
}
// Record a metric if we can attribute this metric to an action (i.e. the
// execution engine is in a relevant state)
if (state_for_metrics != ExecutionEngine::State::kToolInvoke &&
state_for_metrics != ExecutionEngine::State::kUiPostInvoke) {
return;
}
if (is_pre_approved) {
base::UmaHistogramBoolean(kActionNavigationsApprovedByServerHistogram,
true);
return;
}
if (!task_->delegate()) {
return;
}
task_->delegate()->RequestToConfirmNavigation(
task_->id(), destination,
base::BindOnce(
[](webui::mojom::NavigationConfirmationResponsePtr response) {
if (response->result->is_permission_granted()) {
base::UmaHistogramBoolean(
kActionNavigationsApprovedByServerHistogram,
response->result->get_permission_granted());
}
}));
}
void ExecutionEngine::OnNavigationConfirmationDecision(
const url::Origin& destination,
ukm::SourceId ukm_source_id,
base::ScopedUmaHistogramTimer timer,
ExecutionEngine::NavigationDecisionCallback callback,
webui::mojom::NavigationConfirmationResponsePtr response) {
if (response->result->is_permission_granted()) {
bool permission_granted = response->result->get_permission_granted();
// TODO(dylancutler): Separate Actor.NavigationGating.PermissionGranted into
// separate histograms for different confirmation types.
base::UmaHistogramBoolean(kPermissionGrantedHistogram, permission_granted);
ukm::builders::Actor_OriginGating builder(ukm_source_id);
builder
.SetServerConfirmationResult(static_cast<int64_t>(
permission_granted
? ExecutionEngine::ActorServerConfirmationResult::kAccepted
: ExecutionEngine::ActorServerConfirmationResult::kRejected))
.SetEngineState(static_cast<int64_t>(state_));
builder.Record(ukm::UkmRecorder::Get());
permission_granted = permission_granted ||
kGlicConfirmNavigationToNewOriginsDarkLaunch.Get();
if (permission_granted) {
origin_checker_.AllowNavigationTo(destination,
/*is_user_confirmed=*/false);
}
std::move(callback).Run(permission_granted);
return;
}
// TODO(crbug.com/450302860): Add UMA metrics for logging frequency of
// different failure modes.
std::move(callback).Run(/*may_continue=*/false);
}
void ExecutionEngine::SendUserConfirmationDialogRequest(
const url::Origin& destination,
bool for_sensitive_origin,
std::optional<base::ScopedUmaHistogramTimer> timer,
ExecutionEngine::NavigationDecisionCallback callback) {
if (!task_->delegate()) {
std::move(callback).Run(/*may_continue=*/false);
return;
}
journal_->Log(GURL::EmptyGURL(), task_->id(),
"SendUserConfirmationDialogRequest", {});
task_->delegate()->RequestToShowUserConfirmationDialog(
task_->id(), destination, for_sensitive_origin,
base::BindOnce(&ExecutionEngine::OnPromptUserToConfirmNavigationDecision,
GetWeakPtr(), destination, std::move(callback)));
}
void ExecutionEngine::OnPromptUserToConfirmNavigationDecision(
const url::Origin& destination,
ExecutionEngine::NavigationDecisionCallback callback,
webui::mojom::UserConfirmationDialogResponsePtr response) {
if (response->result->is_permission_granted()) {
bool permission_granted = response->result->get_permission_granted();
base::UmaHistogramBoolean(kPermissionGrantedHistogram, permission_granted);
if (permission_granted) {
// See the comment on `OriginOrPrecursorIfOpaque` for why we do not store
// `destination` directly here.
origin_checker_.AllowNavigationTo(OriginOrPrecursorIfOpaque(destination),
/*is_user_confirmed=*/true);
}
std::move(callback).Run(permission_granted);
return;
}
// TODO(crbug.com/450302860): Add UMA metrics for logging frequency of
// different failure modes.
std::move(callback).Run(/*may_continue=*/false);
}
void ExecutionEngine::UserTakeover(
mojom::ActionResultCode takeover_response_code,
base::OnceCallback<void(bool)> callback) {
if (takeover_response_code == mojom::ActionResultCode::kFilePickerTriggered) {
RecordDownloadSaveAsDialogTriggered(true);
}
CancelOngoingActions(takeover_response_code);
// Cancel any existing user takeover callback
RunUserTakeoverCallbackIfExists(/*should_cancel=*/true);
user_takeover_callback_ = std::move(callback);
}
void ExecutionEngine::RunUserTakeoverCallbackIfExists(bool should_cancel) {
if (user_takeover_callback_.is_null()) {
return;
}
std::move(user_takeover_callback_).Run(should_cancel);
}
void ExecutionEngine::AddObserver(StateObserver* observer) {
observers_.AddObserver(observer);
}
void ExecutionEngine::RemoveObserver(StateObserver* observer) {
observers_.RemoveObserver(observer);
}
void ExecutionEngine::DidUninterruptTask() {
if (deferred_finish_tool_invoke_) {
std::move(deferred_finish_tool_invoke_).Run();
}
}
bool ExecutionEngine::TabsCanOpenNewWebContents() const {
return state() == State::kToolInvoke &&
GetInProgressAction().RequiresOpeningWebContents();
}
void ExecutionEngine::CancelOngoingActions(mojom::ActionResultCode reason) {
TRACE_EVENT0("actor", "ExecutionEngine::CancelOngoingActions");
deferred_finish_tool_invoke_.Reset();
if (tool_controller_) {
tool_controller_->Cancel();
}
if (!action_sequence_.empty()) {
CompleteActions(MakeResult(reason), /*action_index=*/std::nullopt);
}
}
void ExecutionEngine::FailCurrentTool(mojom::ActionResultCode reason) {
TRACE_EVENT0("actor", "ExecutionEngine::FailCurrentTool");
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
CHECK_NE(reason, mojom::ActionResultCode::kOk);
if (state_ != State::kToolInvoke) {
return;
}
external_tool_failure_reason_ = reason;
}
void ExecutionEngine::Act(std::vector<std::unique_ptr<ToolRequest>>&& actions,
ActorTask::ActCallback callback) {
TRACE_EVENT0("actor", "ExecutionEngine::Act");
CHECK(base::FeatureList::IsEnabled(features::kGlicActor));
CHECK(!actions.empty());
CHECK(deferred_finish_tool_invoke_.is_null());
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
CHECK_EQ(task_->GetState(), ActorTask::State::kActing);
{
JournalDetailsBuilder journal_details;
for (size_t i = 0; i < actions.size(); ++i) {
journal_details.Add(absl::StrFormat("Actions[%d]", i),
actions[i]->JournalEvent());
}
journal_->Log(GURL::EmptyGURL(), task_->id(), "ExecutionEngine::Act",
std::move(journal_details).Build());
}
if (!action_sequence_.empty()) {
journal_->Log(
actions[0]->GetURLForJournal(), task_->id(), "Act Failed",
JournalDetailsBuilder()
.AddError(
"Unable to perform action: task already has action in progress")
.Build());
PostTaskForActCallback(
std::move(callback),
MakeResultVector(
mojom::ActionResultCode::kExecutionEngineExistingAction));
return;
}
act_callback_ = std::move(callback);
next_action_index_ = 0;
absl::flat_hash_set<int32_t> acting_tab_handles;
action_sequence_ = std::move(actions);
for (const std::unique_ptr<ToolRequest>& action : action_sequence_) {
CHECK(action);
if (action->GetTabHandle() != tabs::TabHandle::Null()) {
acting_tab_handles.insert(action->GetTabHandle().raw_value());
}
if (IsNavigationGatingEnabled() &&
kGlicAllowImplicitToolOriginGrants.Get()) {
if (std::optional<url::Origin> maybe_origin =
action->AssociatedOriginGrant();
maybe_origin) {
origin_checker_.AllowNavigationTo(maybe_origin.value(),
/*is_user_confirmed=*/false);
}
}
}
KickOffNextAction();
}
void ExecutionEngine::KickOffNextAction() {
TRACE_EVENT0("actor", "ExecutionEngine::KickOffNextAction");
DCHECK(state_ == State::kInit || state_ == State::kUiPostInvoke ||
state_ == State::kComplete)
<< "Current state is " << StateToString(state_);
CHECK_LT(next_action_index_, action_sequence_.size());
SetState(State::kStartAction);
if (!GetNextAction().IsFollowup()) {
action_start_time_ = base::TimeTicks::Now();
}
// TODO(b/467984847): ActorTask::AddTab isn't the best way to track a crashed
// tab here. We should refactor this to be more explicit.
if (tabs::TabInterface* tab = GetNextAction().GetTabHandle().Get();
tab && base::FeatureList::IsEnabled(kActorReloadCrashedTabBeforeAct)) {
content::WebContents* contents = tab->GetContents();
CHECK(contents);
if (contents->IsCrashed()) {
GetJournal().Log(
contents->GetLastCommittedURL(), task_->id(),
"ExecutionEngine::KickOffNextAction",
JournalDetailsBuilder().AddError("Renderer crashed").Build());
task_->AddTab(GetNextAction().GetTabHandle(), base::DoNothing());
CompleteActions(MakeResult(mojom::ActionResultCode::kRendererCrashed,
/*requires_page_stabilization=*/false,
"Renderer crashed."),
next_action_index_);
return;
}
}
if (GetNextAction().RequiresUrlCheckInCurrentTab()) {
SafetyChecksForNextAction();
} else {
ExecuteNextAction();
}
}
void ExecutionEngine::SafetyChecksForNextAction() {
TRACE_EVENT0("actor", "ExecutionEngine::SafetyChecksForNextAction");
tabs::TabInterface* tab = GetNextAction().GetTabHandle().Get();
if (!tab) {
journal_->Log(GURL::EmptyGURL(), task_->id(), "Act Failed",
JournalDetailsBuilder()
.AddError("The tab is no longer present")
.Build());
CompleteActions(MakeResult(mojom::ActionResultCode::kTabWentAway,
/*requires_page_stabilization=*/false,
"The tab is no longer present."),
next_action_index_);
return;
}
const SafetyListManager& safety_list_manager =
*SafetyListManager::GetInstance();
const GURL& url =
tab->GetContents()->GetPrimaryMainFrame()->GetLastCommittedURL();
if (safety_list_manager.Find(url, url) ==
SafetyListManager::Decision::kBlock) {
OnMayActOnTabDecision(
tab->GetContents()->GetPrimaryMainFrame()->GetLastCommittedOrigin(),
MayActOnUrlBlockReason::kBlockedByStaticList);
return;
}
// Asynchronously check if we can act on the tab. NOTE that the MayActOnTab
// check uses `GetLastCommittedURL()` from the tab. For opaque origins, this
// means that we'll get the precursor URL. For this reason, we previously
// added the precursor to `origin_checker_` to ensure the optimization guide
// sensitive origin check would be skipped as expected.
MayActOnTab(
*tab, *journal_, task_->id(), origin_checker_, task_->policy_checker(),
base::BindOnce(
&ExecutionEngine::OnMayActOnTabDecision, GetWeakPtr(),
tab->GetContents()->GetPrimaryMainFrame()->GetLastCommittedOrigin()));
}
void ExecutionEngine::OnMayActOnTabDecision(
const url::Origin& evaluated_origin,
MayActOnUrlBlockReason block_reason) {
if (block_reason == MayActOnUrlBlockReason::kOptimizationGuideBlock &&
IsNavigationGatingEnabled() &&
kGlicPromptUserForSensitiveNavigations.Get()) {
auto response_to_result_code = base::BindOnce(
[](MayActOnUrlBlockReason block_reason, bool may_continue) {
return may_continue
? mojom::ActionResultCode::kOk
: BlockReasonToResultCode(block_reason,
/*for_navigation=*/false);
},
block_reason);
SendUserConfirmationDialogRequest(
evaluated_origin,
/*for_sensitive_origin=*/true,
/*timer=*/std::nullopt,
std::move(response_to_result_code)
.Then(base::BindOnce(&ExecutionEngine::DidFinishAsyncSafetyChecks,
GetWeakPtr(), evaluated_origin)));
return;
}
DidFinishAsyncSafetyChecks(
evaluated_origin,
BlockReasonToResultCode(block_reason, /*for_navigation=*/false));
}
void ExecutionEngine::DidFinishAsyncSafetyChecks(
const url::Origin& evaluated_origin,
mojom::ActionResultCode result_code) {
TRACE_EVENT0("actor", "ExecutionEngine::DidFinishAsyncSafetyChecks");
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
CHECK(!action_sequence_.empty());
tabs::TabInterface* tab = GetNextAction().GetTabHandle().Get();
if (!tab) {
journal_->Log(GURL::EmptyGURL(), task_->id(), "Act Failed",
JournalDetailsBuilder()
.AddError("The tab is no longer present")
.Build());
CompleteActions(MakeResult(mojom::ActionResultCode::kTabWentAway,
/*requires_page_stabilization=*/false,
"The tab is no longer present."),
next_action_index_);
return;
}
TaskId task_id = task_->id();
if (!evaluated_origin.IsSameOriginWith(tab->GetContents()
->GetPrimaryMainFrame()
->GetLastCommittedOrigin())) {
// A cross-origin navigation occurred before we got permission. The result
// is no longer applicable. For now just fail.
// TODO(mcnee): Handle this gracefully.
journal_->Log(GetNextAction().GetURLForJournal(), task_id, "Act Failed",
JournalDetailsBuilder()
.AddError("Acting after cross-origin navigation occurred")
.Build());
FailedOnTabBeforeToolCreation();
CompleteActions(MakeResult(mojom::ActionResultCode::kCrossOriginNavigation,
/*requires_page_stabilization=*/false,
"Acting after cross-origin navigation occurred"),
next_action_index_);
return;
}
if (!IsOk(result_code)) {
journal_->Log(
GetNextAction().GetURLForJournal(), task_id, "Act Failed",
JournalDetailsBuilder().AddError("URL blocked for actions").Build());
FailedOnTabBeforeToolCreation();
CompleteActions(MakeResult(result_code,
/*requires_page_stabilization=*/false,
"URL blocked for actions"),
next_action_index_);
return;
}
ExecuteNextAction();
}
void ExecutionEngine::FailedOnTabBeforeToolCreation() {
tabs::TabHandle tab = GetNextAction().GetTabHandle();
journal_->Log(GetNextAction().GetURLForJournal(), task_->id(), "Act Failed",
JournalDetailsBuilder()
.Add("tabId", tab.raw_value())
.AddError("Associating tab for failed action")
.Build());
task_->AddTab(tab, base::DoNothing());
}
void ExecutionEngine::ExecuteNextAction() {
TRACE_EVENT0("actor", "ExecutionEngine::ExecuteNextAction");
DCHECK_EQ(state_, State::kStartAction);
CHECK(!action_sequence_.empty());
CHECK(tool_controller_);
++next_action_index_;
SetState(State::kToolCreateAndVerify);
tool_controller_->CreateToolAndValidate(
GetInProgressAction(),
base::BindOnce(&ExecutionEngine::PostToolCreate, GetWeakPtr()));
}
void ExecutionEngine::PostToolCreate(mojom::ActionResultPtr result) {
TRACE_EVENT0("actor", "ExecutionEngine::PostToolCreate");
if (!IsOk(*result)) {
CompleteActions(std::move(result), InProgressActionIndex());
return;
}
SetState(State::kUiPreInvoke);
ui_event_dispatcher_->OnPreTool(
GetInProgressAction(),
base::BindOnce(&ExecutionEngine::FinishedUiPreInvoke, GetWeakPtr()));
}
void ExecutionEngine::FinishedUiPreInvoke(mojom::ActionResultPtr result) {
TRACE_EVENT0("actor", "ExecutionEngine::FinishedUiPreInvoke");
DCHECK_EQ(state_, State::kUiPreInvoke);
if (!IsOk(*result)) {
CompleteActions(std::move(result), InProgressActionIndex());
return;
}
SetState(State::kToolInvoke);
tool_controller_->Invoke(
base::BindOnce(&ExecutionEngine::FinishedToolInvoke, GetWeakPtr()));
}
void ExecutionEngine::FinishedToolInvoke(mojom::ActionResultPtr result) {
TRACE_EVENT0("actor", "ExecutionEngine::FinishedToolInvoke");
DCHECK_EQ(state_, State::kToolInvoke);
if (tool_invoke_complete_callback_for_testing_) {
std::move(tool_invoke_complete_callback_for_testing_).Run();
}
// If the task is waiting on user input, defer returning a result for it. This
// prevents the actor state from proceeding and also allows UI to insert an
// `external_tool_failure_reason` to the action.
if (base::FeatureList::IsEnabled(kGlicDeferActUntilUninterrupted) &&
task_->GetState() == ActorTask::State::kWaitingOnUser) {
CHECK(deferred_finish_tool_invoke_.is_null());
deferred_finish_tool_invoke_ =
base::BindOnce(&ExecutionEngine::FinishedToolInvoke,
base::Unretained(this), std::move(result));
return;
}
// The current action errored out. Stop the chain.
std::optional<mojom::ActionResultCode> external_tool_failure_reason;
std::swap(external_tool_failure_reason, external_tool_failure_reason_);
if (external_tool_failure_reason) {
CompleteActions(MakeResult(*external_tool_failure_reason),
InProgressActionIndex());
return;
}
if (!IsOk(*result)) {
CompleteActions(std::move(result), InProgressActionIndex());
return;
}
// TODO(bokan): If tool completion is deferred due to interruption (e.g.
// waiting on a user to confirm an action) the recorded tool metrics will look
// inflated. This is a problem even if we record the metrics at the start of
// this function (before deferring) because presumably the tool itself waits
// on the cause of an interruption (and may reach here due to timeout or other
// reason). Ideally we'd split metrics based on whether or not an
// interruption was involved. Will file bug.
CHECK(result->execution_end_time);
base::TimeTicks end_time = base::TimeTicks::Now();
RecordToolTimings(GetInProgressAction().Name(), end_time - action_start_time_,
end_time - *result->execution_end_time);
if (GetInProgressAction().IsFollowup()) {
CHECK(!action_results_.empty());
ActionResultWithLatencyInfo& action_result = action_results_.back();
action_result.result = std::move(result);
action_result.end_time = end_time;
} else {
action_results_.emplace_back(action_start_time_, end_time,
std::move(result));
}
SetState(State::kUiPostInvoke);
ui_event_dispatcher_->OnPostTool(
GetInProgressAction(),
base::BindOnce(&ExecutionEngine::FinishedUiPostInvoke, GetWeakPtr()));
}
void ExecutionEngine::FinishedUiPostInvoke(mojom::ActionResultPtr result) {
TRACE_EVENT0("actor", "ExecutionEngine::FinishedUiPostInvoke");
DCHECK_EQ(state_, State::kUiPostInvoke);
CHECK(!action_sequence_.empty());
CHECK(deferred_finish_tool_invoke_.is_null());
if (!IsOk(*result)) {
CompleteActions(std::move(result), InProgressActionIndex());
return;
}
if (next_action_index_ >= action_sequence_.size()) {
CompleteActions(MakeOkResult(), std::nullopt);
return;
}
KickOffNextAction();
}
void ExecutionEngine::CompleteActions(mojom::ActionResultPtr result,
std::optional<size_t> action_index) {
TRACE_EVENT0("actor", "ExecutionEngine::CompleteActions");
CHECK(!action_sequence_.empty());
CHECK(act_callback_);
// If we have not yet appended the action_results for the failed index,
// append it now.
if (action_index) {
size_t result_index = GetResultIndexForAction(*action_index);
if (action_results_.size() == result_index) {
action_results_.emplace_back(action_start_time_, base::TimeTicks::Now(),
result->Clone());
} else if (action_results_.size() > result_index &&
(!IsOk(*result) ||
action_sequence_[*action_index]->IsFollowup())) {
// If we already have a result for this action, and the new result is an
// error, overwrite it. This can happen if a tool invocation succeeds
// but a subsequent UI post-invoke stage fails, or for follow up tools
// that fail.
ActionResultWithLatencyInfo& action_result =
action_results_[result_index];
action_result.result = result->Clone();
action_result.end_time = base::TimeTicks::Now();
}
} else if (!IsOk(*result)) {
// If it's a general error, we still want it in action_results.
action_results_.emplace_back(action_start_time_, base::TimeTicks::Now(),
result->Clone());
}
SetState(State::kComplete);
action_sequence_ended_callbacks_.Notify(IsOk(*result));
if (!IsOk(*result)) {
GURL url;
if (action_index) {
url = action_sequence_[*action_index]->GetURLForJournal();
}
journal_->Log(
url, task_->id(), "Act Failed",
JournalDetailsBuilder().AddError(ToDebugString(*result)).Build());
}
RecordActionResultCode(result->code);
PostTaskForActCallback(std::move(act_callback_), std::move(action_results_));
action_sequence_.clear();
next_action_index_ = 0;
actions_weak_ptr_factory_.InvalidateWeakPtrs();
}
base::WeakPtr<ExecutionEngine> ExecutionEngine::GetWeakPtr() {
return actions_weak_ptr_factory_.GetWeakPtr();
}
bool ExecutionEngine::HasActionSequence() const {
return !action_sequence_.empty();
}
favicon::FaviconService* ExecutionEngine::GetFaviconService() {
return FaviconServiceFactory::GetForProfile(
task_->GetProfile(), ServiceAccessType::EXPLICIT_ACCESS);
}
void ExecutionEngine::IsAcceptableNavigationDestination(
const GURL& url,
DecisionCallbackWithReason callback) {
MayActOnUrl(url, /*allow_insecure_http=*/true, task_->GetProfile(), *journal_,
task_->id(), task_->policy_checker(), std::move(callback));
}
Profile& ExecutionEngine::GetProfile() {
return *task_->GetProfile();
}
AggregatedJournal& ExecutionEngine::GetJournal() {
return *journal_;
}
actor_login::ActorLoginService& ExecutionEngine::GetActorLoginService() {
return *actor_login_service_;
}
autofill::ActorFormFillingService&
ExecutionEngine::GetActorFormFillingService() {
return *actor_form_filling_service_;
}
void ExecutionEngine::PromptToSelectCredential(
const std::vector<actor_login::Credential>& credentials,
const base::flat_map<std::string, gfx::Image>& icons,
ToolDelegate::CredentialSelectedCallback callback) {
TRACE_EVENT0("actor", "ExecutionEngine::PromptToSelectCredential");
CHECK(!credentials.empty());
if (credential_selection_override_callback_ &&
base::FeatureList::IsEnabled(
password_manager::features::kPasswordCheckupPrototype)) {
std::move(credential_selection_override_callback_)
.Run(credentials, std::move(callback));
return;
}
if (!task_->delegate()) {
// TODO(crbug.com/427817882): Explicit error reason (kNewLonginAttempt).
std::move(callback).Run(/*selected_credential=*/webui::mojom::
SelectCredentialDialogResponse::New());
return;
}
task_->delegate()->RequestToShowCredentialSelectionDialog(
task_->id(), icons, credentials, std::move(callback));
}
void ExecutionEngine::SetUserSelectedCredential(
const ToolDelegate::CredentialWithPermission& credential_with_permission,
base::OnceClosure affiliations_fetched) {
url::Origin origin = credential_with_permission.credential.request_origin;
user_selected_credentials_[origin] = credential_with_permission;
affiliations::AffiliationService* affiliation_service =
AffiliationServiceFactory::GetForProfile(task_->GetProfile());
// Fetch strongly affiliated domains, in order to be able to reuse the
// permission for sites that do not have the exact same origin but are
// strongly affiliated.
if (base::FeatureList::IsEnabled(
password_manager::features::
kActorLoginPermissionsUseStrongAffiliations) &&
affiliation_service) {
affiliation_service->GetAffiliationsAndBranding(
affiliations::FacetURI::FromPotentiallyInvalidSpec(
origin.GetURL().GetWithEmptyPath().spec()),
base::BindOnce(&ExecutionEngine::OnAffiliationsReceived, GetWeakPtr(),
origin, std::move(affiliations_fetched)));
} else {
std::move(affiliations_fetched).Run();
}
}
void ExecutionEngine::OnAffiliationsReceived(
const url::Origin& source_origin,
base::OnceClosure affiliations_fetched,
const std::vector<affiliations::Facet>& results,
bool success) {
if (success) {
for (const auto& facet : results) {
// Iterate through results to find Web facets (format:
// https://<host>[:<port>]) required for actor login. Android facets are
// ignored.
if (!facet.uri.IsValidWebFacetURI()) {
continue;
}
GURL url(facet.uri.canonical_spec());
url::Origin affiliated_origin = url::Origin::Create(url);
if (!affiliated_origin.IsSameOriginWith(source_origin)) {
affiliated_origin_map_[affiliated_origin] = source_origin;
}
}
}
std::move(affiliations_fetched).Run();
}
const std::optional<ToolDelegate::CredentialWithPermission>
ExecutionEngine::GetUserSelectedCredential(
const url::Origin& request_origin) const {
// Try exact match first.
auto it = user_selected_credentials_.find(request_origin);
if (it != user_selected_credentials_.end()) {
return it->second;
}
if (base::FeatureList::IsEnabled(
password_manager::features::
kActorLoginPermissionsUseStrongAffiliations)) {
// Check if the current origin is affiliated with a previously encountered
// one within the current task.
auto aff_it = affiliated_origin_map_.find(request_origin);
if (aff_it != affiliated_origin_map_.end()) {
auto original_cred_it = user_selected_credentials_.find(aff_it->second);
if (original_cred_it != user_selected_credentials_.end()) {
return original_cred_it->second;
}
}
}
return std::nullopt;
}
void ExecutionEngine::RequestToShowAutofillSuggestions(
std::vector<autofill::ActorFormFillingRequest> requests,
base::WeakPtr<AutofillSelectionDialogEventHandler> event_handler,
ExecutionEngine::AutofillSuggestionSelectedCallback callback) {
TRACE_EVENT0("actor", "ExecutionEngine::RequestToShowAutofillSuggestions");
CHECK(!requests.empty());
if (!task_->delegate()) {
std::move(callback).Run(
webui::mojom::SelectAutofillSuggestionsDialogResponse::New(
task_->id().value(),
webui::mojom::SelectAutofillSuggestionsDialogResult::NewErrorReason(
webui::mojom::SelectAutofillSuggestionsDialogErrorReason::
kNoActorTaskDelegate)));
return;
}
task_->delegate()->RequestToShowAutofillSuggestionsDialog(
task_->id(), std::move(requests), std::move(event_handler),
std::move(callback));
task_->action_tracker_for_metrics().OnAutofillAttentionDialogPresented();
}
void ExecutionEngine::InterruptFromTool() {
task_->Interrupt();
}
void ExecutionEngine::UninterruptFromTool() {
task_->Uninterrupt(ActorTask::State::kActing);
}
void ExecutionEngine::EnqueueFollowupAction(
std::unique_ptr<ToolRequest> action) {
action->SetAsFollowup(base::PassKey<ExecutionEngine>());
action_sequence_.insert(action_sequence_.begin() + next_action_index_,
std::move(action));
}
base::WeakPtr<actor_login::ActionSequenceDelegate>
ExecutionEngine::GetActionSequenceDelegate() {
return actions_weak_ptr_factory_.GetWeakPtr();
}
base::CallbackListSubscription ExecutionEngine::RegisterActionSequenceEnded(
base::OnceCallback<void(bool)> callback) {
return action_sequence_ended_callbacks_.Add(std::move(callback));
}
void ExecutionEngine::OnFederatedLoginOutcome(
actor_login::LoginStatusResult result) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
mojom::ActionResultCode code =
AttemptLoginTool::LoginResultToActorResult(result);
if (!IsOk(code)) {
FailCurrentTool(code);
}
}
void ExecutionEngine::AddWritableMainframeOrigins(
const absl::flat_hash_set<url::Origin>& added_writable_mainframe_origins) {
if (!IsNavigationGatingEnabled()) {
return;
}
origin_checker_.AllowNavigationTo(added_writable_mainframe_origins);
}
void ExecutionEngine::PreHandleCredentialSelectionDialog(
CredentialSelectionOverrideCallback callback) {
credential_selection_override_callback_ = std::move(callback);
}
const ToolRequest& ExecutionEngine::GetNextAction() const {
CHECK_LT(next_action_index_, action_sequence_.size());
return *action_sequence_.at(next_action_index_).get();
}
size_t ExecutionEngine::InProgressActionIndex() const {
CHECK(state_ == State::kUiPreInvoke || state_ == State::kToolInvoke ||
state_ == State::kUiPostInvoke || state_ == State::kToolCreateAndVerify)
<< "Current state is " << StateToString(state_);
CHECK_GT(next_action_index_, 0ul);
return next_action_index_ - 1;
}
const ToolRequest& ExecutionEngine::GetInProgressAction() const {
return *action_sequence_.at(InProgressActionIndex()).get();
}
size_t ExecutionEngine::GetResultIndexForAction(size_t action_index) const {
CHECK_LT(action_index, action_sequence_.size());
size_t original_count = std::count_if(
action_sequence_.begin(), action_sequence_.begin() + action_index + 1,
[](const std::unique_ptr<ToolRequest>& action) {
return !action->IsFollowup();
});
// At least the first action could not be a follow up.
CHECK_GT(original_count, 0ul);
return original_count - 1;
}
std::ostream& operator<<(std::ostream& o, const ExecutionEngine::State& s) {
return o << ExecutionEngine::StateToString(s);
}
} // namespace actor