Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed

- Antigravity is retired to a non-runnable legacy tombstone: it is no longer
selectable from the provider picker, `--provider`, `/provider`,
`CODEWHALE_PROVIDER`, `config set provider`, or route ids; the private
Antigravity OAuth-token import, `AGY_ADC_AUTH`, `ANTIGRAVITY_API_KEY`, and
the cloud-code transport are removed; the catalog export and public docs
omit it (`scripts/check-provider-registry.py` fails if it reappears).
Existing `[providers.antigravity]` / `[providers.agy]` tables still parse
so `codewhale auth clear --provider antigravity` can forget only
Codewhale-owned state (config table, fallback entries, top-level selection,
Codewhale's own secret slot, and the sibling `config.toml.bak`); it never
reads, revokes, or alters a Google or Antigravity session. Gemini routes
through the supported `google` provider with `GEMINI_API_KEY`.
- Provider-native web search now applies domain constraints before accepting an
attempt, discards generated answers when returned citations violate those
constraints, and preserves the caller's configured/local timeout as an
Expand Down
10 changes: 10 additions & 0 deletions config.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -814,6 +814,16 @@ max_subagents = 10 # optional (default 64, clamped to 1-128)
# base_url = "https://api.mistral.ai/v1"
# model = "mistral-code-latest" # or mistral-medium-latest, mistral-small-latest, mistral-large-latest

# Google Gemini — Google AI Studio (https://aistudio.google.com/apikey)
# OpenAI-compatible Chat Completions route on the official Gemini endpoint;
# this is the supported Gemini path (see docs/PROVIDERS.md).
# Provider aliases: google, gemini, google-gemini, ai-studio
# Env var aliases: GEMINI_API_KEY, GOOGLE_API_KEY, GEMINI_BASE_URL, GOOGLE_BASE_URL
[providers.google]
# api_key = "YOUR_GEMINI_API_KEY"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocker This changed line looks like a hardcoded secret.

# base_url = "https://generativelanguage.googleapis.com/v1beta/openai/"
# model = "gemini-3.1-pro-preview"

# ─────────────────────────────────────────────────────────────────────────────────
# Alibaba Cloud Model Studio — Token Plan
# (https://bailian.console.aliyun.com/)
Expand Down
365 changes: 356 additions & 9 deletions crates/cli/src/lib.rs

Large diffs are not rendered by default.

45 changes: 3 additions & 42 deletions crates/config/src/external_credentials.rs
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,8 @@ pub enum ExternalCredentialSource {
GrokCli,
/// Official DeepSeek Harness (`dsh`) `$DSH_HOME/.credentials.yaml`.
DshCli,
/// Official Antigravity CLI (`agy`) `state.vscdb` OAuth token.
/// Legacy tombstone retained only so old Codewhale consent records can
/// deserialize and be cleared. No runtime may resolve or read this source.
AgyCli,
}

Expand All @@ -197,46 +198,6 @@ pub fn default_dsh_credentials_path() -> PathBuf {
home.join(".credentials.yaml")
}

/// Default Antigravity credential store, resolved without probing: the
/// official `agy` CLI persists its OAuth token in the Antigravity app's
/// VSCode-style `state.vscdb` under the user profile. Consent is pinned to
/// this exact path; an ambient move is reported, never followed.
#[must_use]
pub fn default_agy_credentials_path() -> PathBuf {
let base = match std::env::var_os("ANTIGRAVITY_STATE_DIR") {
Some(value) if !value.is_empty() => PathBuf::from(value),
_ => agy_profile_base(),
};
base.join("User").join("globalStorage").join("state.vscdb")
}

#[cfg(target_os = "macos")]
fn agy_profile_base() -> PathBuf {
codewhale_paths::user_home()
.unwrap_or_else(|| PathBuf::from("."))
.join("Library/Application Support/Antigravity")
}

#[cfg(all(unix, not(target_os = "macos")))]
fn agy_profile_base() -> PathBuf {
match std::env::var_os("XDG_CONFIG_HOME") {
Some(value) if !value.is_empty() => PathBuf::from(value),
_ => codewhale_paths::user_home()
.unwrap_or_else(|| PathBuf::from("."))
.join(".config"),
}
.join("Antigravity")
}

#[cfg(windows)]
fn agy_profile_base() -> PathBuf {
match std::env::var_os("APPDATA") {
Some(value) if !value.is_empty() => PathBuf::from(value),
_ => codewhale_paths::user_home().unwrap_or_else(|| PathBuf::from(".")),
}
.join("Antigravity")
}

impl ExternalCredentialSource {
#[must_use]
pub const fn as_str(self) -> &'static str {
Expand All @@ -257,7 +218,7 @@ impl ExternalCredentialSource {
Self::KimiCodeCli => "Kimi Code CLI",
Self::GrokCli => "Grok CLI",
Self::DshCli => "DeepSeek Harness",
Self::AgyCli => "Antigravity CLI",
Self::AgyCli => "retired Antigravity consent",
}
}
}
Expand Down
101 changes: 86 additions & 15 deletions crates/config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,8 @@ pub use codewhale_secrets::Secrets;
pub use external_credentials::{
EXTERNAL_CREDENTIAL_CONSENT_VERSION, EXTERNAL_CREDENTIAL_READ_ONLY_SEMANTICS,
ExternalCredentialAccess, ExternalCredentialConsentStatus, ExternalCredentialConsentToml,
ExternalCredentialReadGrant, ExternalCredentialSource, default_agy_credentials_path,
default_dsh_credentials_path, external_credential_consent_status, quote_os_path,
resolve_external_credential_path,
ExternalCredentialReadGrant, ExternalCredentialSource, default_dsh_credentials_path,
external_credential_consent_status, quote_os_path, resolve_external_credential_path,
};
use serde::{Deserialize, Serialize};
use sha2::{Digest as _, Sha256};
Expand All @@ -90,6 +89,7 @@ use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};

pub const CONFIG_FILE_NAME: &str = "config.toml";
pub const PERMISSIONS_FILE_NAME: &str = "permissions.toml";
pub const LEGACY_ANTIGRAVITY_TOMBSTONE_MESSAGE: &str = "Antigravity is a retired, non-runnable legacy provider. Clear Codewhale-owned legacy state with `codewhale auth clear --provider antigravity`; this does not alter Google or Antigravity sessions. For Gemini use provider `google` with `GEMINI_API_KEY`.";

/// Secret-store routing metadata; never credential material.
pub const API_KEYRING_SENTINEL: &str = "__KEYRING__";
Expand Down Expand Up @@ -442,8 +442,8 @@ pub struct ProvidersToml {
alias = "gemini"
)]
pub google: ProviderConfigToml,
/// Google Antigravity (`agy`) — consent-gated credential import only;
/// sends fail closed until the cloud-code wire protocol exists.
/// Retired Antigravity configuration. This table exists only so old
/// Codewhale-owned state can deserialize and be cleared safely.
#[serde(
default,
skip_serializing_if = "ProviderConfigToml::is_empty",
Expand Down Expand Up @@ -645,6 +645,7 @@ impl ProvidersToml {
&& ProviderKind::all()
.iter()
.all(|provider| self.for_provider(*provider).is_empty())
&& self.antigravity.is_empty()
}

#[must_use]
Expand Down Expand Up @@ -1022,6 +1023,9 @@ fn set_provider_config_value(
field: ProviderConfigField,
value: &str,
) -> Result<()> {
if provider == ProviderKind::Antigravity {
bail!(LEGACY_ANTIGRAVITY_TOMBSTONE_MESSAGE);
}
match field {
ProviderConfigField::ApiKey => {
let value = value.to_string();
Expand Down Expand Up @@ -2961,6 +2965,11 @@ impl ConfigToml {
}

pub fn set_value(&mut self, key: &str, value: &str) -> Result<()> {
if parse_custom_provider_config_key(key).is_some_and(|(provider_id, _)| {
ProviderKind::parse_config_identity(provider_id) == Some(ProviderKind::Antigravity)
}) {
bail!(LEGACY_ANTIGRAVITY_TOMBSTONE_MESSAGE);
}
if let Some((provider, field)) = parse_provider_config_key(key) {
return set_provider_config_value(self, provider, field, value);
}
Expand All @@ -2971,6 +2980,9 @@ impl ConfigToml {
match key {
"provider" => {
if let Some(provider) = ProviderKind::parse_config_identity(value) {
if provider == ProviderKind::Antigravity {
bail!(LEGACY_ANTIGRAVITY_TOMBSTONE_MESSAGE);
}
self.provider = provider;
self.selected_provider_id = None;
} else {
Expand Down Expand Up @@ -5464,6 +5476,73 @@ pub fn scrub_plaintext_api_keys_from_config_backup(path: &Path) -> Result<()> {
Ok(())
}

/// Remove only retired Antigravity state from Codewhale's one-time config
/// backup. This never resolves, reads, writes, or revokes any external Google
/// or Antigravity session; it edits only the checked sibling `.bak` file owned
/// by Codewhale.
pub fn scrub_legacy_antigravity_from_config_backup(path: &Path) -> Result<()> {
let backup = checked_config_backup_path(path)?;
if !backup.exists() {
return Ok(());
}

let raw = read_checked_toml_file(&backup, "config backup")?;
let scrubbed = config_toml_without_legacy_antigravity(&raw).with_context(|| {
format!(
"failed to clear retired provider state from config backup {}",
backup.display()
)
})?;
if scrubbed != raw {
persistence::atomic_write(&backup, scrubbed.as_bytes()).with_context(|| {
format!(
"failed to write retired-provider-free config backup {}",
backup.display()
)
})?;
}
Ok(())
}

fn config_toml_without_legacy_antigravity(raw: &str) -> Result<String> {
let mut document = raw.parse::<toml_edit::DocumentMut>().map_err(|_| {
anyhow::anyhow!(
"failed to parse config TOML while clearing retired provider state; file contents were omitted"
)
})?;
let root = document.as_table_mut();

if root
.get("provider")
.and_then(toml_edit::Item::as_str)
.is_some_and(is_legacy_antigravity_name)
{
root.remove("provider");
}
if let Some(fallbacks) = root
.get_mut("fallback_providers")
.and_then(toml_edit::Item::as_array_mut)
{
fallbacks.retain(|value| !value.as_str().is_some_and(is_legacy_antigravity_name));
if fallbacks.is_empty() {
root.remove("fallback_providers");
}
}
if let Some(providers) = root
.get_mut("providers")
.and_then(toml_edit::Item::as_table_like_mut)
{
providers.remove("antigravity");
providers.remove("agy");
}

Ok(document.to_string())
}

fn is_legacy_antigravity_name(value: &str) -> bool {
value.eq_ignore_ascii_case("antigravity") || value.eq_ignore_ascii_case("agy")
}

fn write_one_time_config_backup(path: &Path) -> Result<()> {
let backup = checked_config_backup_path(path)?;
if backup.exists() {
Expand Down Expand Up @@ -6909,8 +6988,6 @@ struct EnvRuntimeOverrides {
mistral_model: Option<String>,
google_base_url: Option<String>,
google_model: Option<String>,
antigravity_base_url: Option<String>,
antigravity_model: Option<String>,
telecomjs_base_url: Option<String>,
telecomjs_model: Option<String>,
edenai_base_url: Option<String>,
Expand Down Expand Up @@ -7229,12 +7306,6 @@ impl EnvRuntimeOverrides {
xai_model: std::env::var("XAI_MODEL")
.ok()
.filter(|v| !v.trim().is_empty()),
antigravity_base_url: std::env::var("ANTIGRAVITY_BASE_URL")
.ok()
.filter(|v| !v.trim().is_empty()),
antigravity_model: std::env::var("ANTIGRAVITY_MODEL")
.ok()
.filter(|v| !v.trim().is_empty()),
google_base_url: std::env::var("GOOGLE_BASE_URL")
.ok()
.filter(|v| !v.trim().is_empty())
Expand Down Expand Up @@ -7348,7 +7419,7 @@ impl EnvRuntimeOverrides {
ProviderKind::Xai => self.xai_base_url.clone(),
ProviderKind::Mistral => self.mistral_base_url.clone(),
ProviderKind::Google => self.google_base_url.clone(),
ProviderKind::Antigravity => self.antigravity_base_url.clone(),
ProviderKind::Antigravity => None,
ProviderKind::Telecomjs => self.telecomjs_base_url.clone(),
ProviderKind::Edenai => self.edenai_base_url.clone(),
ProviderKind::ModelstudioTokenPlan | ProviderKind::ModelstudioTokenPlanAnthropic => {
Expand Down Expand Up @@ -7395,7 +7466,7 @@ impl EnvRuntimeOverrides {
ProviderKind::Xai => self.xai_model.clone(),
ProviderKind::Mistral => self.mistral_model.clone(),
ProviderKind::Google => self.google_model.clone(),
ProviderKind::Antigravity => self.antigravity_model.clone(),
ProviderKind::Antigravity => None,
ProviderKind::Telecomjs => self.telecomjs_model.clone(),
ProviderKind::Edenai => self.edenai_model.clone(),
ProviderKind::ModelstudioTokenPlan | ProviderKind::ModelstudioTokenPlanAnthropic => {
Expand Down
Loading
Loading