blob: 920d37be8aafe2d1a979f632858d184cba42f05f [file] [log] [blame]
// 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 <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/callback.h"
#include "base/logging.h"
#include "base/memory/weak_ptr.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/histogram_macros.h"
#include "base/no_destructor.h"
#include "base/notimplemented.h"
#include "base/state_transitions.h"
#include "base/trace_event/trace_event.h"
#include "base/types/id_type.h"
#include "base/types/optional_ref.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_policy_checker.h"
#include "chrome/browser/actor/actor_task.h"
#include "chrome/browser/actor/actor_util.h"
#include "chrome/browser/actor/browser_action_util.h"
#include "chrome/browser/actor/safety_list_manager.h"
#include "chrome/browser/actor/site_policy.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/glic/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/browser/ui/browser_navigator.h"
#include "chrome/browser/ui/browser_navigator_params.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/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 "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 {
BASE_FEATURE(kActorReloadCrashedTabBeforeAct, base::FEATURE_ENABLED_BY_DEFAULT);
const RenderFrameHost* GetPrimaryMainFrame(
content::NavigationHandle& navigation_handle) {
return navigation_handle.GetWebContents()->GetPrimaryMainFrame();
}
void PostTaskForActCallback(
ActorTask::ActCallback callback,
mojom::ActionResultPtr result,
std::optional<size_t> index_of_failed_action,
std::vector<ActionResultWithLatencyInfo> action_results) {
RecordActionResultCode(result->code);
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
base::BindOnce(std::move(callback), std::move(result),
index_of_failed_action, std::move(action_results)));
}
// Helper to determine if we should gate and thus send an IPC when navigtating
// to a new origin. See note on `kGlicNavigationGatingUseSiteNotOrigin`.
bool IsSameForNewOriginNavigationGating(const url::Origin& reference_origin,
const GURL& navigation_url) {
CHECK(IsNavigationGatingEnabled());
if (kGlicNavigationGatingUseSiteNotOrigin.Get()) {
return net::SchemefulSite::IsSameSite(reference_origin.GetURL(),
navigation_url);
}
return reference_origin.IsSameOriginWith(navigation_url);
}
// 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;
ExecutionEngine::ExecutionEngine(Profile* profile)
: profile_(profile),
journal_(ActorKeyedService::Get(profile)->GetJournal().GetSafeRef()),
ui_event_dispatcher_(ui::NewUiEventDispatcher(
ActorKeyedService::Get(profile)->GetActorUiStateManager())) {
TRACE_EVENT0("actor", "ExecutionEngine::ExecutionEngine");
CHECK(profile_);
}
ExecutionEngine::ExecutionEngine(
Profile* profile,
std::unique_ptr<ui::UiEventDispatcher> ui_event_dispatcher)
: profile_(profile),
journal_(ActorKeyedService::Get(profile)->GetJournal().GetSafeRef()),
ui_event_dispatcher_(std::move(ui_event_dispatcher)) {
TRACE_EVENT0("actor", "ExecutionEngine::ExecutionEngine");
CHECK(profile_);
}
std::unique_ptr<ExecutionEngine> ExecutionEngine::CreateForTesting(
Profile* profile,
std::unique_ptr<ui::UiEventDispatcher> ui_event_dispatcher) {
return base::WrapUnique<ExecutionEngine>(
new ExecutionEngine(profile, std::move(ui_event_dispatcher)));
}
ExecutionEngine::~ExecutionEngine() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
RecordActorNavigationGatingListSize(
allowed_navigation_origins_->size(),
user_confirmed_blocklisted_origins_->size());
RunUserTakeoverCallbackIfExists(/*should_cancel=*/true);
}
void ExecutionEngine::SetOwner(ActorTask* task) {
task_ = task;
TRACE_EVENT0("actor", "ExecutionEngine::SetOwner");
actor_login_service_ = std::make_unique<actor_login::ActorLoginServiceImpl>();
actor_form_filling_service_ =
std::make_unique<autofill::ActorFormFillingServiceImpl>();
tool_controller_ = std::make_unique<ToolController>(*task_, *this);
}
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";
}
}
bool ExecutionEngine::ShouldGateNavigation(
content::NavigationHandle& navigation_handle,
ExecutionEngine::NavigationDecisionCallback callback) {
if (!IsNavigationGatingEnabled()) {
return false;
}
CHECK(navigation_handle.GetNavigatingFrameType() ==
content::FrameType::kPrimaryMainFrame ||
navigation_handle.GetNavigatingFrameType() ==
content::FrameType::kPrerenderMainFrame);
GatingDecision decision =
ShouldGateNavigationInternal(navigation_handle, std::move(callback));
if (decision == GatingDecision::kNeedsAsyncCheck) {
return true;
}
bool applied_gate = decision == GatingDecision::kBlockByStaticList;
LogNavigationGating(
/*initiator_origin=*/GetPrimaryMainFrame(navigation_handle)
->GetLastCommittedOrigin(),
navigation_handle.GetURL(), applied_gate);
return applied_gate;
}
ExecutionEngine::GatingDecision ExecutionEngine::ShouldGateNavigationInternal(
content::NavigationHandle& navigation_handle,
ExecutionEngine::NavigationDecisionCallback callback) {
CHECK(!navigation_handle.HasCommitted());
base::ScopedUmaHistogramTimer timer(
"Actor.NavigationGating.TimeElapsedForGating");
const GURL source_url =
GetPrimaryMainFrame(navigation_handle)->GetLastCommittedURL();
const GURL& destination_url = navigation_handle.GetURL();
const GatingDecision decision =
DetermineGatingDecision(source_url, destination_url);
RecordNavigationGatingDecision(decision);
if (decision == GatingDecision::kBlockByStaticList) {
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, base::BindOnce(std::move(callback), /*may_continue=*/false));
} else if (decision == GatingDecision::kNeedsAsyncCheck) {
bool skip_prompt = navigation_handle.IsInPrerenderedMainFrame();
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
base::BindOnce(&ExecutionEngine::CheckNavigationBlocklist, GetWeakPtr(),
navigation_handle.GetInitiatorOrigin(), destination_url,
skip_prompt, std::move(callback)));
}
return decision;
}
void ExecutionEngine::LogNavigationGating(
base::optional_ref<const url::Origin> initiator_origin,
const GURL& navigation_url,
bool applied_gate) {
UMA_HISTOGRAM_BOOLEAN("Actor.NavigationGating.AppliedGate", applied_gate);
if (initiator_origin) {
UMA_HISTOGRAM_BOOLEAN("Actor.NavigationGating.CrossOrigin",
!initiator_origin->IsSameOriginWith(
url::Origin::Create(navigation_url)));
UMA_HISTOGRAM_BOOLEAN("Actor.NavigationGating.CrossSite",
!net::SchemefulSite::IsSameSite(
initiator_origin->GetURL(), navigation_url));
}
}
ExecutionEngine::GatingDecision ExecutionEngine::DetermineGatingDecision(
const GURL& source_url,
const GURL& destination_url) const {
// If enterprise policy allows the destination, do not gate.
// Note that it is not necessary to have an equivalent check for the
// enterprise policy blocklist, as we would already have blocked the
// navigation before reaching this gating logic.
const EnterprisePolicyBlockReason enterprise_reason =
ActorKeyedService::Get(profile_)
->GetPolicyChecker()
.EvaluateEnterprisePolicyForUrl(destination_url);
if (enterprise_reason == EnterprisePolicyBlockReason::kExplicitlyAllowed) {
return GatingDecision::kAllowByStaticList;
}
DCHECK_NE(enterprise_reason, EnterprisePolicyBlockReason::kExplicitlyBlocked);
url::Origin destination_origin = url::Origin::Create(destination_url);
const SafetyListManager& safety_list_manager =
*SafetyListManager::GetInstance();
if (url::IsSameOriginWith(source_url, destination_url)) {
// The static blocklist can still block same-origin navigations. A wildcard
// source entry like `[*, foo.com]` will block a `foo.com -> foo.com`
// navigation. The reasoning is that a URL globally blocked as a
// destination should not be reachable from anywhere, including itself.
// Conversely, a source-specific entry like `[foo.com, *]` will *not* block
// a `foo.com -> foo.com` navigation. This is because a global block on
// navigations *from* a URL is intended to prevent leaving that origin, not
// moving within it.
return safety_list_manager.get_blocked_list()
.ContainsUrlPairWithWildcardSource(source_url,
destination_url)
? GatingDecision::kBlockByStaticList
: GatingDecision::kAllowSameOrigin;
}
if (safety_list_manager.get_blocked_list().ContainsUrlPair(source_url,
destination_url)) {
return GatingDecision::kBlockByStaticList;
}
if (safety_list_manager.get_allowed_list().ContainsUrlPair(source_url,
destination_url)) {
return GatingDecision::kAllowByStaticList;
}
return GatingDecision::kNeedsAsyncCheck;
}
void ExecutionEngine::CheckNavigationBlocklist(
base::optional_ref<const url::Origin> initiator_origin,
const GURL& navigation_url,
bool skip_prompt,
ExecutionEngine::NavigationDecisionCallback callback) {
// Check previously confirmed origins on the sensitive blocklist. If the user
// has previously confirmed the origin is allowed, we should proceed and not
// double prompt.
for (const auto& origin : *user_confirmed_blocklisted_origins_) {
if (origin.IsSameOriginWith(navigation_url)) {
OnNavigationBlocklistDecision(initiator_origin, navigation_url,
skip_prompt, std::move(callback),
/*not_on_blocklist=*/true);
return;
}
}
auto [callback1, callback2] = base::SplitOnceCallback(std::move(callback));
if (ShouldBlockNavigationUrlForOriginGating(
navigation_url, profile_,
base::BindOnce(&ExecutionEngine::OnNavigationBlocklistDecision,
GetWeakPtr(), initiator_origin, navigation_url,
skip_prompt, std::move(callback1)))) {
return;
}
// If `ShouldBlockNavigationUrlForOriginGating` returns false, it means the
// Optimization Guide was not available to check the blocklist, so we
// continue to the next step.
OnNavigationBlocklistDecision(initiator_origin, navigation_url, skip_prompt,
std::move(callback2),
/*not_on_blocklist=*/true);
}
void ExecutionEngine::OnNavigationBlocklistDecision(
base::optional_ref<const url::Origin> initiator_origin,
const GURL navigation_url,
bool skip_prompt,
ExecutionEngine::NavigationDecisionCallback callback,
bool not_on_blocklist) {
// If not blocked by blocklist, check if it's in origin the actor has
// previously interacted with or received instructions from the server to
// interact with.
if (not_on_blocklist) {
if (initiator_origin && IsSameForNewOriginNavigationGating(
initiator_origin.value(), navigation_url)) {
LogNavigationGating(initiator_origin, navigation_url,
/*applied_gate=*/false);
std::move(callback).Run(/*may_continue=*/true);
return;
}
for (const auto& origin : *allowed_navigation_origins_) {
if (IsSameForNewOriginNavigationGating(origin, navigation_url)) {
LogNavigationGating(initiator_origin, navigation_url,
/*applied_gate=*/false);
std::move(callback).Run(/*may_continue=*/true);
return;
}
}
}
// At this point, the navigation is either blocked OR not on the allowlist.
LogNavigationGating(initiator_origin, navigation_url, /*applied_gate=*/true);
if (skip_prompt) {
std::move(callback).Run(/*may_continue=*/false);
return;
}
// If the site is not on the blocklist, 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_on_blocklist) {
HandleNavigationToNewOrigin(url::Origin::Create(navigation_url),
std::move(callback));
return;
}
// We use `kGlicPromptUserForSensitiveNavigations` to toggle user
// confirmations when navigationg to a URL on the optimization guide
// blocklist.
if (!kGlicPromptUserForSensitiveNavigations.Get()) {
std::move(callback).Run(/*may_continue=*/false);
return;
}
// Otherwise if the site is blocked, present a user confirmation dialog to
// continue.
SendUserConfirmationDialogRequest(url::Origin::Create(navigation_url),
/*for_blocklisted_origin=*/true,
std::move(callback));
}
void ExecutionEngine::HandleNavigationToNewOrigin(
const url::Origin& navigation_origin,
ExecutionEngine::NavigationDecisionCallback callback) {
if (!kGlicConfirmNavigationToNewOrigins.Get()) {
std::move(callback).Run(/*may_continue=*/true);
return;
}
if (kGlicPromptUserForNavigationToNewOrigins.Get()) {
SendUserConfirmationDialogRequest(navigation_origin,
/*for_blocklisted_origin=*/false,
std::move(callback));
return;
}
SendNavigationConfirmationRequest(navigation_origin, std::move(callback));
}
void ExecutionEngine::SendNavigationConfirmationRequest(
const url::Origin& navigation_origin,
ExecutionEngine::NavigationDecisionCallback callback) {
if (!task_->delegate()) {
std::move(callback).Run(/*may_continue=*/false);
return;
}
task_->delegate()->RequestToConfirmNavigation(
task_->id(), navigation_origin,
base::BindOnce(&ExecutionEngine::OnNavigationConfirmationDecision,
GetWeakPtr(), navigation_origin, std::move(callback)));
}
void ExecutionEngine::OnNavigationConfirmationDecision(
url::Origin navigation_origin,
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.
UMA_HISTOGRAM_BOOLEAN("Actor.NavigationGating.PermissionGranted",
permission_granted);
if (permission_granted) {
allowed_navigation_origins_->insert(std::move(navigation_origin));
}
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& navigation_origin,
bool for_blocklisted_origin,
ExecutionEngine::NavigationDecisionCallback callback) {
if (!task_->delegate()) {
std::move(callback).Run(/*may_continue=*/false);
return;
}
task_->delegate()->RequestToShowUserConfirmationDialog(
task_->id(), navigation_origin, for_blocklisted_origin,
base::BindOnce(&ExecutionEngine::OnPromptUserToConfirmNavigationDecision,
GetWeakPtr(), navigation_origin, for_blocklisted_origin,
std::move(callback)));
}
void ExecutionEngine::OnPromptUserToConfirmNavigationDecision(
url::Origin navigation_origin,
bool for_blocklisted_origin,
ExecutionEngine::NavigationDecisionCallback callback,
webui::mojom::UserConfirmationDialogResponsePtr response) {
if (response->result->is_permission_granted()) {
bool permission_granted = response->result->get_permission_granted();
UMA_HISTOGRAM_BOOLEAN("Actor.NavigationGating.PermissionGranted",
permission_granted);
if (permission_granted) {
// See the comment on `OriginOrPrecursorIfOpaque` for why we do not store
// `navigation_origin` directly here and for the confirmed blocklist
// origins.
allowed_navigation_origins_->insert(
OriginOrPrecursorIfOpaque(navigation_origin));
// We update both lists in the `for_blocklisted_origin` case so that we do
// not have to double-confirm this origin when we invoke
// ExecutionEngine::HandleNavigationToNewOrigin.
if (for_blocklisted_origin) {
user_confirmed_blocklisted_origins_->insert(
OriginOrPrecursorIfOpaque(navigation_origin));
}
}
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::CancelOngoingActions(mojom::ActionResultCode reason) {
TRACE_EVENT0("actor", "ExecutionEngine::CancelOngoingActions");
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());
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),
MakeResult(mojom::ActionResultCode::kExecutionEngineExistingAction),
std::nullopt, {});
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()) {
if (std::optional<url::Origin> maybe_origin =
action->AssociatedOriginGrant();
maybe_origin) {
allowed_navigation_origins_->insert(maybe_origin.value());
}
}
}
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);
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;
}
// 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 used the
// precusor in `user_confirmed_blocklisted_origins_` to ensure the
// optimization blocklist check would be skipped as expected.
ActorKeyedService::Get(profile_)->GetPolicyChecker().MayActOnTab(
*tab, *journal_, task_->id(), user_confirmed_blocklisted_origins_,
base::BindOnce(
&ExecutionEngine::OnMayActOnTabDecision, GetWeakPtr(),
tab->GetContents()->GetPrimaryMainFrame()->GetLastCommittedOrigin()));
}
void ExecutionEngine::OnMayActOnTabDecision(
const url::Origin& evaluated_origin,
MayActOnUrlBlockReason block_reason) {
switch (block_reason) {
case MayActOnUrlBlockReason::kAllowed:
DidFinishAsyncSafetyChecks(evaluated_origin, /*may_act=*/true);
return;
case MayActOnUrlBlockReason::kOptimizationGuideBlock:
if (IsNavigationGatingEnabled() &&
kGlicPromptUserForSensitiveNavigations.Get()) {
SendUserConfirmationDialogRequest(
evaluated_origin,
/*for_blocklisted_origin=*/true,
base::BindOnce(&ExecutionEngine::DidFinishAsyncSafetyChecks,
GetWeakPtr(), evaluated_origin));
return;
}
[[fallthrough]];
case MayActOnUrlBlockReason::kActuactionDisabled:
case MayActOnUrlBlockReason::kExternalProtocol:
case MayActOnUrlBlockReason::kIpAddress:
case MayActOnUrlBlockReason::kLookalikeDomain:
case MayActOnUrlBlockReason::kSafeBrowsing:
case MayActOnUrlBlockReason::kTabIsErrorDocument:
case MayActOnUrlBlockReason::kUrlNotInAllowlist:
case MayActOnUrlBlockReason::kWrongScheme:
case MayActOnUrlBlockReason::kEnterprisePolicy:
DidFinishAsyncSafetyChecks(evaluated_origin, /*may_act=*/false);
}
}
void ExecutionEngine::DidFinishAsyncSafetyChecks(
const url::Origin& evaluated_origin,
bool may_act) {
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 (!may_act) {
journal_->Log(
GetNextAction().GetURLForJournal(), task_id, "Act Failed",
JournalDetailsBuilder().AddError("URL blocked for actions").Build());
FailedOnTabBeforeToolCreation();
CompleteActions(MakeResult(mojom::ActionResultCode::kUrlBlocked,
/*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);
// 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;
}
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);
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());
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 && action_results_.size() == *action_index) {
action_results_.emplace_back(action_start_time_, base::TimeTicks::Now(),
result->Clone());
}
SetState(State::kComplete);
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());
}
PostTaskForActCallback(std::move(act_callback_), std::move(result),
action_index, 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(
profile_, ServiceAccessType::EXPLICIT_ACCESS);
}
void ExecutionEngine::IsAcceptableNavigationDestination(
const GURL& url,
DecisionCallbackWithReason callback) {
ActorKeyedService::Get(profile_)->GetPolicyChecker().MayActOnUrl(
url, /*allow_insecure_http=*/true, profile_, *journal_, task_->id(),
std::move(callback));
}
Profile& ExecutionEngine::GetProfile() {
return *profile_;
}
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 (!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(profile_);
// 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,
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(callback));
}
void ExecutionEngine::InterruptFromTool() {
task_->Interrupt();
}
void ExecutionEngine::UninterruptFromTool() {
task_->Uninterrupt(ActorTask::State::kActing);
}
void ExecutionEngine::AddWritableMainframeOrigins(
const absl::flat_hash_set<url::Origin>& added_writable_mainframe_origins) {
if (!IsNavigationGatingEnabled()) {
return;
}
for (const auto& origin : added_writable_mainframe_origins) {
// Intentionally storing a copy of the origin so that ExecutionEngine owns
// the url::Origin's stored in allowed_navigation_origins_.
allowed_navigation_origins_->insert(url::Origin(origin));
}
}
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();
}
std::ostream& operator<<(std::ostream& o, const ExecutionEngine::State& s) {
return o << ExecutionEngine::StateToString(s);
}
} // namespace actor