blob: ca3774710b68147655545e3f67f6883c84eb164f [file]
// Copyright 2026 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/skills/internal/skills_downloader.h"
#include "base/functional/bind.h"
#include "components/skills/proto/skill.pb.h"
#include "mojo/public/cpp/bindings/callback_helpers.h"
#include "net/base/load_flags.h"
#include "net/http/http_request_headers.h"
#include "net/http/http_status_code.h"
#include "net/traffic_annotation/network_traffic_annotation.h"
#include "services/network/public/cpp/resource_request.h"
#include "services/network/public/cpp/simple_url_loader.h"
#include "services/network/public/mojom/url_response_head.mojom.h"
namespace skills {
namespace {
constexpr size_t kMaxDownloadBytes = 1024 * 1024; // 1MB
// Max number of retries for fetching the configuration.
constexpr int kMaxRetries = 3;
// The conditions for which a retry will trigger.
constexpr int kRetryMode = network::SimpleURLLoader::RETRY_ON_5XX |
network::SimpleURLLoader::RETRY_ON_NETWORK_CHANGE |
network::SimpleURLLoader::RETRY_ON_NAME_NOT_RESOLVED;
constexpr net::NetworkTrafficAnnotationTag kSkillsDownloaderNetworkTag =
net::DefineNetworkTrafficAnnotation("skills_service_loader", R"(
semantics {
sender: "Skills Service"
description:
"Downloads a list of first-party agent prompts, also called skills, "
"that are accessible to users via chrome://skills/discover-skills."
trigger:
"Triggered on chrome://skills/discover-skills page load or attempt "
"to save a skill."
data: "None"
destination: GOOGLE_OWNED_SERVICE
user_data {
type: NONE
}
internal {
contacts {
email: "//components/skills/OWNERS"
}
}
last_reviewed: "2026-01-27"
}
policy {
cookies_allowed: NO
setting: "This feature cannot be disabled in settings."
policy_exception_justification: "World-readable data is being "
"downloaded to be displayed on all Chrome accounts."
})");
inline constexpr char kSkillsDownloaderGstaticUrl[] =
"https://www.gstatic.com/chrome/skills/first_party_skills_binary";
std::unique_ptr<network::SimpleURLLoader> CreateSimpleURLLoader(
const GURL& url,
const std::string_view last_modified_header) {
auto resource_request = std::make_unique<network::ResourceRequest>();
resource_request->url = url;
resource_request->method = net::HttpRequestHeaders::kGetMethod;
resource_request->credentials_mode = network::mojom::CredentialsMode::kOmit;
resource_request->headers.SetHeader(net::HttpRequestHeaders::kIfModifiedSince,
last_modified_header);
return network::SimpleURLLoader::Create(std::move(resource_request),
kSkillsDownloaderNetworkTag);
}
} // namespace
SkillsDownloader::SkillsDownloader(
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory)
: url_loader_factory_(std::move(url_loader_factory)) {}
SkillsDownloader::~SkillsDownloader() = default;
void SkillsDownloader::FetchDiscoverySkills(OnFetchCompleteCallback callback) {
auto wrapped_callback =
mojo::WrapCallbackWithDefaultInvokeIfNotRun(std::move(callback), nullptr);
// If there is already a request in progress, don't try again.
if (!url_loader_factory_ || pending_request_) {
return;
}
GURL destination = GURL(kSkillsDownloaderGstaticUrl);
pending_request_ = CreateSimpleURLLoader(destination, last_modified_header_);
pending_request_->SetRetryOptions(kMaxRetries, kRetryMode);
pending_request_->DownloadToString(
url_loader_factory_.get(),
base::BindOnce(&SkillsDownloader::OnUrlDownloadComplete,
self_ptr_factory_.GetWeakPtr(),
std::move(wrapped_callback)),
kMaxDownloadBytes);
}
void SkillsDownloader::OnUrlDownloadComplete(
OnFetchCompleteCallback callback,
std::optional<std::string> response_body) {
// Move the pending request here so it's deleted when this function ends.
std::unique_ptr<network::SimpleURLLoader> request =
std::move(pending_request_);
if (!request) {
return;
}
// Response code is not 200 or `response_body` is empty.
if (!request->ResponseInfo() || !request->ResponseInfo()->headers ||
request->ResponseInfo()->headers->response_code() != net::HTTP_OK ||
!response_body.has_value() || response_body->empty()) {
// TODO(crbug.com/478924560): Add metrics to track network failures and
// other error cases.
return;
}
auto skills_list = std::make_unique<skills::proto::SkillsList>();
if (!skills_list->ParseFromString(response_body.value())) {
return;
}
std::string last_modified_value;
if (request->ResponseInfo()->headers->EnumerateHeader(
nullptr, "Last-Modified", &last_modified_value)) {
last_modified_header_ = last_modified_value;
}
auto skills_map = std::make_unique<SkillsMap>();
for (auto& skill : *skills_list->mutable_skills()) {
skills_map->insert_or_assign(skill.id(), std::move(skill));
}
std::move(callback).Run(std::move(skills_map));
}
} // namespace skills