blob: afbcf71eb817c64875003386556e11abbe2bd6cd [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
//! Fixed official distributions; no caller-controlled URLs or destinations.
use crate::Agent;
use maka_capabilities::call::Scope;
use maka_capabilities::http;
use maka_capabilities::storage::Data;
use maka_capabilities::storage::Directory;
use maka_capabilities::storage::Mutation;
use maka_capabilities::storage::Store;
use maka_capabilities::storage::StoreError;
use sha2::{Digest, Sha256};
use std::{collections::BTreeMap, future::Future, io::Write, time::Duration};
use tokio_util::sync::CancellationToken;
mod archive;
const VERSION: &str = "1.2.1";
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("Antigravity is unavailable on this platform")]
Unsupported,
#[error("Antigravity installation cancelled")]
Cancelled,
#[error("Antigravity download failed")]
Download,
#[error("Antigravity distribution integrity check failed")]
Integrity,
#[error("Antigravity installation file operation failed")]
Files(#[from] std::io::Error),
#[error("Antigravity private storage unavailable")]
Storage(#[from] maka_capabilities::storage::StoreError),
#[error("Antigravity installation cleanup is unconfirmed")]
Cleanup,
#[error("Antigravity was installed but its temporary download could not be removed")]
StagingCleanup,
}
#[derive(Clone)]
struct Release {
platform: &'static str,
url: String,
server: &'static str,
helper: &'static str,
// Other official registry platforms have no upstream digest. Their first
// HTTPS download digest is recorded, not misrepresented as a pinned hash.
pinned: Option<&'static str>,
}
impl Release {
fn current() -> Result<Self, Error> {
Self::for_platform(std::env::consts::OS, std::env::consts::ARCH)
}
fn for_platform(os: &str, arch: &str) -> Result<Self, Error> {
let (directory, platform) = match (os, arch) {
("macos", "aarch64") => ("macos", "darwin-arm64"),
("macos", "x86_64") => ("macos", "darwin-x86_64"),
("linux", "aarch64") => ("linux", "linux-arm64"),
("linux", "x86_64") => ("linux", "linux-x86_64"),
("windows", "aarch64") => ("windows", "windows-arm64"),
("windows", "x86_64") => ("windows", "windows-x86_64"),
_ => return Err(Error::Unsupported),
};
Ok(Self {
platform,
url: format!(
"https://dl.google.com/agy-extensions/releases/{directory}/agy-acp-server-{VERSION}-{platform}.zip"
),
server: if os == "windows" {
"agy_acp_server.exe"
} else {
"agy_acp_server.par"
},
helper: if os == "windows" {
"localharness_external.exe"
} else {
"localharness_external"
},
pinned: (platform == "darwin-arm64")
.then_some("0fab9938812e6b32b3b543e65e4f3a0025ceef755413db13542d9a9b81ea803c"),
})
}
fn destination(&self) -> String {
format!("antigravity-{VERSION}-{}", self.platform)
}
fn agent(&self, root: &std::path::Path) -> Agent {
let directory = root.join(self.destination());
let mut env = BTreeMap::from([
(
"ANTIGRAVITY_HARNESS_PATH".into(),
directory.join(self.helper).to_string_lossy().into_owned(),
),
("PYTHONUNBUFFERED".into(), "1".into()),
]);
// Verified Python browser suppression on Unix. Do not invent an
// equivalent Windows command; the authentication bridge owns its UI.
if !self.platform.starts_with("windows") {
env.insert("BROWSER".into(), "/usr/bin/true".into());
}
Agent {
id: "antigravity-acp".into(),
display_name: "Antigravity".into(),
executable: directory.join(self.server).to_string_lossy().into_owned(),
args: if self.platform.starts_with("linux") {
vec!["--uid=".into()]
} else {
vec![]
},
env,
}
}
}
#[derive(Clone)]
struct Control {
cancellation: CancellationToken,
}
impl Control {
fn check(&self, retiring: &CancellationToken) -> Result<(), Error> {
if self.cancellation.is_cancelled() || retiring.is_cancelled() {
return Err(Error::Cancelled);
}
Ok(())
}
async fn wait<T>(&self, work: impl Future<Output = T>) -> Result<T, Error> {
tokio::select! {
biased;
_ = self.cancellation.cancelled() => Err(Error::Cancelled),
value = work => Ok(value),
}
}
}
/// Install or verify the immutable current-platform release in plugin private
/// storage. The caller owns admission and saving the returned Agent configuration.
pub async fn antigravity(
storage: &dyn Store,
http: &dyn http::Client,
data: &Directory,
call: Scope,
cancellation: &CancellationToken,
) -> Result<Agent, Error> {
let release = Release::current()?;
let control = Control {
cancellation: cancellation.clone(),
};
let root = data.read_only().await?.location();
let key = format!("install/{}", release.destination());
// Private files are writable by the external process. Only Host KV may
// anchor executable identity; cache-side metadata cannot confer trust.
let record = storage.read(key.clone()).await?;
let anchor = record
.as_ref()
.and_then(|row| row.data.value())
.map(|value| {
serde_json::from_value::<archive::Receipt>(value.clone()).map_err(|_| Error::Integrity)
})
.transpose()?;
let existing = if let Some(receipt) = anchor {
let release = release.clone();
let control = control.clone();
data.run(move |dir, retiring| {
archive::existing(dir, &release, &receipt, &control, retiring)
})
.await??
} else {
false
};
if existing {
return Ok(release.agent(&root));
}
let staging = format!(".antigravity-{}", uuid::Uuid::new_v4());
let result = async {
let staging = staging.clone();
let control = control.clone();
data.run(move |dir, retiring| {
control.check(retiring)?;
archive::prepare(dir, &staging)
})
.await??;
Ok::<_, Error>(())
}
.await;
let result = match result {
Ok(()) => install(http, data, call, &release, &staging, &control).await,
Err(error) => Err(error),
};
let result = match result {
// Once files have been published, finish the accepted KV commit even
// when caller cancellation arrives. A crash before it causes a fresh
// official download on retry, never adoption of an unanchored cache.
Ok(receipt) => save_receipt(storage, key, record.map(|row| row.revision), receipt).await,
Err(error) => Err(error),
};
// Only this attempt's UUID directory is removed. No concurrent install's
// staging or published version can be collected by another attempt.
let cleanup = data
.run(move |dir, _| match dir.remove_dir_all(staging) {
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
result => result,
})
.await;
if !matches!(cleanup, Ok(Ok(()))) {
// Retirement can deny private-file cleanup after all active workers
// have settled. Disk residue is not an unowned network/process effect.
// Preserve the original failure (including cancellation) when present.
return match result {
Err(error) => Err(error),
Ok(()) => Err(Error::StagingCleanup),
};
}
result?;
Ok(release.agent(&root))
}
async fn install(
http: &dyn http::Client,
data: &Directory,
call: Scope,
release: &Release,
staging: &str,
control: &Control,
) -> Result<archive::Receipt, Error> {
let response = control
.wait(http.request(
call,
http::Request {
url: release.url.clone(),
method: http::Method::Get,
headers: vec![],
body: vec![],
},
))
.await?
.map_err(|_| Error::Download)?;
let result = async {
if response.head.status != 200 || response.head.url != release.url {
return Err(Error::Download);
}
let mut bytes = 0_u64;
let mut hash = Sha256::new();
while let Some(chunk) = control
.wait(response.body.next())
.await?
.map_err(|_| Error::Download)?
{
bytes = bytes
.checked_add(chunk.len() as u64)
.ok_or(Error::Integrity)?;
hash.update(&chunk);
let path = format!("{staging}/download.zip");
let control = control.clone();
data.run(move |dir, retiring| {
control.check(retiring)?;
let mut options = cap_std::fs::OpenOptions::new();
options.append(true);
dir.open_with(path, &options)?.write_all(&chunk)?;
Ok::<_, Error>(())
})
.await??;
}
let digest = hex::encode(hash.finalize());
if bytes == 0
|| release.pinned.is_some_and(|pinned| pinned != digest)
|| (release.platform == "darwin-arm64" && bytes != 111_725_488)
{
return Err(Error::Integrity);
}
let release = release.clone();
let staging = staging.to_owned();
let control = control.clone();
let receipt = data
.run(move |dir, retiring| {
archive::publish(dir, &release, &staging, bytes, digest, &control, retiring)
})
.await??;
Ok(receipt)
}
.await;
if result.is_err() {
response.body.cancel();
}
if !matches!(
tokio::time::timeout(Duration::from_secs(5), response.body.close()).await,
Ok(Ok(()))
) {
return Err(Error::Cleanup);
}
result
}
async fn save_receipt(
store: &dyn Store,
key: String,
revision: Option<u64>,
receipt: archive::Receipt,
) -> Result<(), Error> {
let value = serde_json::to_value(receipt).map_err(|_| Error::Integrity)?;
match store
.batch(vec![Mutation {
key: key.clone(),
expected_revision: revision,
data: Data::Present(value.clone()),
}])
.await
{
Ok(_) => Ok(()),
Err(error @ StoreError::Conflict { .. }) => {
let winner = store.read(key).await?;
if winner.as_ref().and_then(|row| row.data.value()) == Some(&value) {
Ok(())
} else {
Err(error.into())
}
}
Err(error) => Err(error.into()),
}
}