feat(providers): Antigravity (agy 1.1.13) credential plane, fail-closed sends

Antigravity joins as its own provider (kind `antigravity`, alias `agy`),
separate from Google. v0.9.8 scope is the credential plane with the same
honesty as the dsh / Codex CLI / Grok CLI external-credential contracts:

- Consent: reading anything requires an explicit
  `codewhale auth external-consent` grant pinned to the exact
  per-OS `state.vscdb` path of the official Antigravity app profile
  (`default_agy_credentials_path`). Absent consent means disabled; no
  discovery, no probing.
- Read-only: the SQLite store is opened read-only through the shared
  secure no-follow boundary, size-capped at 64 MiB, with a header check
  and an inode recheck across the query. Codewhale never writes,
  refreshes, or re-authenticates the foreign file.
- Extraction: only the documented `antigravityUnifiedStateSync.oauthToken`
  ItemTable key; JSON-wrapped access tokens or bare tokens accepted,
  anything else fails closed. Not an embed of any other harness.
- Precedence: Codewhale-owned `ANTIGRAVITY_API_KEY` beats the process's
  own `AGY_ADC_AUTH`, which beats the consented file.

Sends fail closed before any body is built: the cloud-code wire protocol
is not implemented, and pretending it is OpenAI-compatible would ship
credentials to a route that cannot serve them. The error tells the user
to use the `google` provider for Gemini models.

Seven focused tests (fixture SQLite stores built by the tests, no
network): extraction, JSON member extraction, missing row, malformed
JSON, wrong grant source, precedence order, empty-token handling.
Provider registries, picker tables, CLI arg, consent source
(`agy_cli`), and PROVIDERS.md rows are wired (40 catalog kinds / 45
registry entries). Live calls: none — no Antigravity credentials were
used in this environment.
This commit is contained in:
CodeWhale Bot committed 2026-08-14 02:36:32 -07:00
1 parent 0889eb53cc
commit 531b64ed9d
16 files changed
+612 -13

No files matched your search

+72
View File
@@ -66,6 +66,14 @@ templates moved to v0.9.9.
auto/low/medium/high; first-party DeepSeek keeps a documented `low` tier.
`/effort` persists and receipts through the same path as Ctrl+T.
- Google Gemini is its own backend (`/provider google`) on the official
OpenAI-compatible route with thought-signature capture/replay and
fail-closed replay for thinking models. Antigravity (`agy` 1.1.13) joins
as a separate credential-plane provider: consent-gated read-only import
of the official CLI's login with `ANTIGRAVITY_API_KEY`/`AGY_ADC_AUTH`
precedence; requests fail closed until the cloud-code wire protocol is
implemented.
### Removed
- The source-structure budget ratchet (CI step, checker, baseline JSON).
@@ -367,6 +375,14 @@ runs against Pi 0.8.41 and by dogfooding repeated manual compaction.
publishes and checks one multi-architecture manifest. It no longer rebuilds
both targets through the single long-running QEMU job that lost its runner.
- Google Gemini is its own backend (`/provider google`) on the official
OpenAI-compatible route with thought-signature capture/replay and
fail-closed replay for thinking models. Antigravity (`agy` 1.1.13) joins
as a separate credential-plane provider: consent-gated read-only import
of the official CLI's login with `ANTIGRAVITY_API_KEY`/`AGY_ADC_AUTH`
precedence; requests fail closed until the cloud-code wire protocol is
implemented.
### Removed
- The no-progress guard, repeated-read guard, and injected tool-error strategy
@@ -910,6 +926,14 @@ File edits, terminal width, and Windows installation.
zero could loop indefinitely), restoring a zero-advisory `npm audit` for
the website.
- Google Gemini is its own backend (`/provider google`) on the official
OpenAI-compatible route with thought-signature capture/replay and
fail-closed replay for thinking models. Antigravity (`agy` 1.1.13) joins
as a separate credential-plane provider: consent-gated read-only import
of the official CLI's login with `ANTIGRAVITY_API_KEY`/`AGY_ADC_AUTH`
precedence; requests fail closed until the cloud-code wire protocol is
implemented.
### Removed
- The default model-facing SlopLedger implementation, its storage-oriented
@@ -1016,6 +1040,14 @@ stale runtime and dependency surface.
- Bracketed-paste contents are redacted from traces, and credential diagnostics
never treat placeholder sentinels as usable keys.
- Google Gemini is its own backend (`/provider google`) on the official
OpenAI-compatible route with thought-signature capture/replay and
fail-closed replay for thinking models. Antigravity (`agy` 1.1.13) joins
as a separate credential-plane provider: consent-gated read-only import
of the official CLI's login with `ANTIGRAVITY_API_KEY`/`AGY_ADC_AUTH`
precedence; requests fail closed until the cloud-code wire protocol is
implemented.
### Removed
- The legacy callable aliases `exec_shell`, `run_shell_command`, `read_file`,
@@ -2508,6 +2540,14 @@ largest curated model-and-pricing expansion in the project so far.
show a one-shot deprecation notice. Removal is deferred beyond v0.9.0 so
this release does not break existing scripts without a dedicated cutover.
- Google Gemini is its own backend (`/provider google`) on the official
OpenAI-compatible route with thought-signature capture/replay and
fail-closed replay for thinking models. Antigravity (`agy` 1.1.13) joins
as a separate credential-plane provider: consent-gated read-only import
of the official CLI's login with `ANTIGRAVITY_API_KEY`/`AGY_ADC_AUTH`
precedence; requests fail closed until the cloud-code wire protocol is
implemented.
### Removed
- Remove the deprecated `deepseek` and `deepseek-tui` binary shims in this
@@ -3005,6 +3045,14 @@ reproductions shaped v0.9.0:
- Added an untrusted constitution-draft gate with authoring provenance so
model-drafted constitutions require explicit human ratification.
- Google Gemini is its own backend (`/provider google`) on the official
OpenAI-compatible route with thought-signature capture/replay and
fail-closed replay for thinking models. Antigravity (`agy` 1.1.13) joins
as a separate credential-plane provider: consent-gated read-only import
of the official CLI's login with `ANTIGRAVITY_API_KEY`/`AGY_ADC_AUTH`
precedence; requests fail closed until the cloud-code wire protocol is
implemented.
### Removed
- Removed unused model-registry helpers. Harvested from #3872 by @cyq1017.
@@ -4034,6 +4082,14 @@ folds in several community contributions.
- Stale `--bin deepseek` examples, `DeepSeek-TUI` strings in `/change`
output, and pre-rebrand doc comments.
- Google Gemini is its own backend (`/provider google`) on the official
OpenAI-compatible route with thought-signature capture/replay and
fail-closed replay for thinking models. Antigravity (`agy` 1.1.13) joins
as a separate credential-plane provider: consent-gated read-only import
of the official CLI's login with `ANTIGRAVITY_API_KEY`/`AGY_ADC_AUTH`
precedence; requests fail closed until the cloud-code wire protocol is
implemented.
### Removed
- Unused dependencies: `tracing-appender` and `zeroize` (TUI crate),
@@ -4685,6 +4741,14 @@ patches, retesting, and release-stabilization signals that shaped this pass.
- The invalid-model error now names the active provider and lists Arcee among
the options.
- Google Gemini is its own backend (`/provider google`) on the official
OpenAI-compatible route with thought-signature capture/replay and
fail-closed replay for thinking models. Antigravity (`agy` 1.1.13) joins
as a separate credential-plane provider: consent-gated read-only import
of the official CLI's login with `ANTIGRAVITY_API_KEY`/`AGY_ADC_AUTH`
precedence; requests fail closed until the cloud-code wire protocol is
implemented.
### Removed
- **The session "cycle" / checkpoint-restart system.** Removed the `/cycles`,
@@ -4879,6 +4943,14 @@ release: **@A-Corner** (#2438), **@taiwan988** (#2483), **@AiurArtanis**
event envelopes, task migration/env isolation fixes, and state-message
parent IDs for future forks (#2383, #2358, #2367, #2252, #2272, #2308).
- Google Gemini is its own backend (`/provider google`) on the official
OpenAI-compatible route with thought-signature capture/replay and
fail-closed replay for thinking models. Antigravity (`agy` 1.1.13) joins
as a separate credential-plane provider: consent-gated read-only import
of the official CLI's login with `ANTIGRAVITY_API_KEY`/`AGY_ADC_AUTH`
precedence; requests fail closed until the cloud-code wire protocol is
implemented.
### Removed
- **Qwen 3.7 Max OpenRouter preset.** Removed from the model registry, docs,
+44
View File
@@ -177,6 +177,8 @@ pub enum ExternalCredentialSource {
GrokCli,
/// Official DeepSeek Harness (`dsh`) `$DSH_HOME/.credentials.yaml`.
DshCli,
/// Official Antigravity CLI (`agy`) `state.vscdb` OAuth token.
AgyCli,
}
/// Default DeepSeek Harness credentials document, resolved without probing.
@@ -195,6 +197,46 @@ 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 {
@@ -203,6 +245,7 @@ impl ExternalCredentialSource {
Self::KimiCodeCli => "kimi_code_cli",
Self::GrokCli => "grok_cli",
Self::DshCli => "dsh_cli",
Self::AgyCli => "agy_cli",
}
}
@@ -214,6 +257,7 @@ impl ExternalCredentialSource {
Self::KimiCodeCli => "Kimi Code CLI",
Self::GrokCli => "Grok CLI",
Self::DshCli => "DeepSeek Harness",
Self::AgyCli => "Antigravity CLI",
}
}
}
+23
View File
@@ -422,6 +422,14 @@ 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.
#[serde(
default,
skip_serializing_if = "ProviderConfigToml::is_empty",
alias = "agy"
)]
pub antigravity: ProviderConfigToml,
/// Jiangsu Telecom TokenHub — OpenAI-compatible AI gateway.
#[serde(
default,
@@ -652,6 +660,7 @@ impl ProvidersToml {
ProviderKind::Xai => &self.xai,
ProviderKind::Mistral => &self.mistral,
ProviderKind::Google => &self.google,
ProviderKind::Antigravity => &self.antigravity,
ProviderKind::Telecomjs => &self.telecomjs,
ProviderKind::ModelstudioTokenPlan => &self.modelstudio_token_plan,
ProviderKind::ModelstudioTokenPlanAnthropic => &self.modelstudio_token_plan_anthropic,
@@ -701,6 +710,7 @@ impl ProvidersToml {
ProviderKind::Xai => &mut self.xai,
ProviderKind::Mistral => &mut self.mistral,
ProviderKind::Google => &mut self.google,
ProviderKind::Antigravity => &mut self.antigravity,
ProviderKind::Telecomjs => &mut self.telecomjs,
ProviderKind::ModelstudioTokenPlan => &mut self.modelstudio_token_plan,
ProviderKind::ModelstudioTokenPlanAnthropic => {
@@ -3222,6 +3232,7 @@ impl ConfigToml {
ProviderKind::Xai => DEFAULT_XAI_BASE_URL.to_string(),
ProviderKind::Mistral => DEFAULT_MISTRAL_BASE_URL.to_string(),
ProviderKind::Google => DEFAULT_GOOGLE_BASE_URL.to_string(),
ProviderKind::Antigravity => DEFAULT_ANTIGRAVITY_BASE_URL.to_string(),
ProviderKind::Telecomjs => DEFAULT_TELECOMJS_BASE_URL.to_string(),
ProviderKind::ModelstudioTokenPlan
| ProviderKind::ModelstudioTokenPlanAnthropic
@@ -4314,6 +4325,7 @@ fn default_model_for_provider(provider: ProviderKind) -> &'static str {
ProviderKind::Xai => DEFAULT_XAI_MODEL,
ProviderKind::Mistral => DEFAULT_MISTRAL_MODEL,
ProviderKind::Google => DEFAULT_GOOGLE_MODEL,
ProviderKind::Antigravity => DEFAULT_ANTIGRAVITY_MODEL,
ProviderKind::Telecomjs => DEFAULT_TELECOMJS_MODEL,
ProviderKind::ModelstudioTokenPlan
| ProviderKind::ModelstudioTokenPlanAnthropic
@@ -4364,6 +4376,7 @@ fn default_base_url_for_provider(provider: ProviderKind) -> &'static str {
ProviderKind::Xai => DEFAULT_XAI_BASE_URL,
ProviderKind::Mistral => DEFAULT_MISTRAL_BASE_URL,
ProviderKind::Google => DEFAULT_GOOGLE_BASE_URL,
ProviderKind::Antigravity => DEFAULT_ANTIGRAVITY_BASE_URL,
ProviderKind::Telecomjs => DEFAULT_TELECOMJS_BASE_URL,
ProviderKind::ModelstudioTokenPlan => DEFAULT_MODELSTUDIO_TOKEN_PLAN_BASE_URL,
ProviderKind::ModelstudioTokenPlanAnthropic => MODELSTUDIO_TOKEN_PLAN_ANTHROPIC_BASE_URL,
@@ -6625,6 +6638,8 @@ 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>,
modelstudio_token_plan_base_url: Option<String>,
@@ -6935,6 +6950,12 @@ 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())
@@ -7060,6 +7081,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::Telecomjs => self.telecomjs_base_url.clone(),
ProviderKind::ModelstudioTokenPlan | ProviderKind::ModelstudioTokenPlanAnthropic => {
self.modelstudio_token_plan_base_url.clone()
@@ -7105,6 +7127,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::Telecomjs => self.telecomjs_model.clone(),
ProviderKind::ModelstudioTokenPlan | ProviderKind::ModelstudioTokenPlanAnthropic => {
self.modelstudio_token_plan_model.clone()
+28 -8
View File
@@ -5,13 +5,13 @@
//! providers; runtime routing remains in `ConfigToml::resolve_runtime_options`.
use super::{
DEFAULT_ARCEE_BASE_URL, DEFAULT_ARCEE_MODEL, DEFAULT_ATLASCLOUD_BASE_URL,
DEFAULT_ATLASCLOUD_MODEL, DEFAULT_DEEPINFRA_BASE_URL, DEFAULT_DEEPINFRA_MODEL,
DEFAULT_DEEPSEEK_ANTHROPIC_BASE_URL, DEFAULT_DEEPSEEK_ANTHROPIC_MODEL,
DEFAULT_DEEPSEEK_BASE_URL, DEFAULT_DEEPSEEK_MODEL, DEFAULT_FIREWORKS_BASE_URL,
DEFAULT_FIREWORKS_MODEL, DEFAULT_GOOGLE_BASE_URL, DEFAULT_GOOGLE_MODEL,
DEFAULT_HUGGINGFACE_BASE_URL, DEFAULT_HUGGINGFACE_MODEL, DEFAULT_LONGCAT_BASE_URL,
DEFAULT_LONGCAT_MODEL, DEFAULT_META_BASE_URL, DEFAULT_META_MODEL,
DEFAULT_ANTIGRAVITY_BASE_URL, DEFAULT_ANTIGRAVITY_MODEL, DEFAULT_ARCEE_BASE_URL,
DEFAULT_ARCEE_MODEL, DEFAULT_ATLASCLOUD_BASE_URL, DEFAULT_ATLASCLOUD_MODEL,
DEFAULT_DEEPINFRA_BASE_URL, DEFAULT_DEEPINFRA_MODEL, DEFAULT_DEEPSEEK_ANTHROPIC_BASE_URL,
DEFAULT_DEEPSEEK_ANTHROPIC_MODEL, DEFAULT_DEEPSEEK_BASE_URL, DEFAULT_DEEPSEEK_MODEL,
DEFAULT_FIREWORKS_BASE_URL, DEFAULT_FIREWORKS_MODEL, DEFAULT_GOOGLE_BASE_URL,
DEFAULT_GOOGLE_MODEL, DEFAULT_HUGGINGFACE_BASE_URL, DEFAULT_HUGGINGFACE_MODEL,
DEFAULT_LONGCAT_BASE_URL, DEFAULT_LONGCAT_MODEL, DEFAULT_META_BASE_URL, DEFAULT_META_MODEL,
DEFAULT_MINIMAX_ANTHROPIC_BASE_URL, DEFAULT_MINIMAX_BASE_URL, DEFAULT_MINIMAX_MODEL,
DEFAULT_MISTRAL_BASE_URL, DEFAULT_MISTRAL_MODEL, DEFAULT_MODELSTUDIO_CODING_PLAN_BASE_URL,
DEFAULT_MODELSTUDIO_TOKEN_PLAN_BASE_URL, DEFAULT_MODELSTUDIO_TOKEN_PLAN_MODEL,
@@ -421,6 +421,12 @@ pub const fn credential_help(kind: ProviderKind) -> CredentialHelp {
docs_url: Some("https://www.alibabacloud.com/help/en/model-studio/"),
guidance: "Sign in to Alibaba Cloud Model Studio (Bailian console), create or copy an API key, and select the plan endpoint matching your subscription (Token Plan or Coding Plan).",
},
ProviderKind::Antigravity => CredentialHelp {
acquisition: OAuth,
credential_url: None,
docs_url: Some("https://antigravity.google/docs/cli/reference"),
guidance: "Sign in with the official agy CLI (1.1.13). Codewhale can read that login's token read-only from the exact pinned state.vscdb after `codewhale auth external-consent`; it never writes or refreshes it. An ANTIGRAVITY_API_KEY or AGY_ADC_AUTH in the process wins over the file.",
},
ProviderKind::Google => CredentialHelp {
acquisition: ApiKey,
credential_url: Some("https://aistudio.google.com/apikey"),
@@ -959,6 +965,18 @@ provider!(
aliases: ["mistral-ai", "mistral_ai", "mistralai", "la-plateforme", "la_plateforme"]
);
provider!(
Antigravity,
Antigravity,
"antigravity",
"Google Antigravity",
DEFAULT_ANTIGRAVITY_BASE_URL,
DEFAULT_ANTIGRAVITY_MODEL,
["ANTIGRAVITY_API_KEY"],
"antigravity",
aliases: ["agy"]
);
provider!(
Google,
Google,
@@ -1590,6 +1608,7 @@ static OPENCODE_ZEN: OpencodeZen = OpencodeZen;
static META: Meta = Meta;
static XAI: Xai = Xai;
static MISTRAL: Mistral = Mistral;
static ANTIGRAVITY: Antigravity = Antigravity;
static TELECOMJS: Telecomjs = Telecomjs;
static MODELSTUDIO_TOKEN_PLAN: ModelstudioTokenPlan = ModelstudioTokenPlan;
static MODELSTUDIO_TOKEN_PLAN_ANTHROPIC: ModelstudioTokenPlanAnthropic =
@@ -1599,7 +1618,7 @@ static MODELSTUDIO_CODING_PLAN_ANTHROPIC: ModelstudioCodingPlanAnthropic =
ModelstudioCodingPlanAnthropic;
static CUSTOM: Custom = Custom;
static PROVIDER_REGISTRY: [&dyn Provider; 44] = [
static PROVIDER_REGISTRY: [&dyn Provider; 45] = [
&DEEPSEEK,
&DEEPSEEK_ANTHROPIC,
&NVIDIA_NIM,
@@ -1643,6 +1662,7 @@ static PROVIDER_REGISTRY: [&dyn Provider; 44] = [
&MODELSTUDIO_CODING_PLAN,
&MODELSTUDIO_CODING_PLAN_ANTHROPIC,
&Google,
&ANTIGRAVITY,
&CUSTOM,
];
+6
View File
@@ -196,3 +196,9 @@ pub const DEFAULT_GOOGLE_BASE_URL: &str =
"https://generativelanguage.googleapis.com/v1beta/openai/";
/// Default Gemini model for the Google provider (preview flagship, 2026-08).
pub const DEFAULT_GOOGLE_MODEL: &str = "gemini-3.1-pro-preview";
/// Antigravity cloud-code internal endpoint (credential plane only; the
/// wire protocol is not implemented and sends fail closed).
pub const DEFAULT_ANTIGRAVITY_BASE_URL: &str = "https://cloudcode-pa.googleapis.com/v1internal";
/// Placeholder model id; never sent — the route fails closed before transport.
pub const DEFAULT_ANTIGRAVITY_MODEL: &str = "gemini-3-pro-preview";
+7 -1
View File
@@ -189,6 +189,11 @@ pub enum ProviderKind {
alias = "alibaba-coding-plan-anthropic"
)]
ModelstudioCodingPlanAnthropic,
/// Google Antigravity (`agy` CLI) — consent-gated read-only credential
/// import only; the cloud-code wire protocol is not implemented and
/// requests fail closed with an actionable message.
#[serde(alias = "agy")]
Antigravity,
/// Google — Gemini OpenAI-compatible endpoint. Its own backend, not an
/// OpenAI alias: thought signatures on tool calls are captured and
/// replayed per Google's contract.
@@ -219,7 +224,7 @@ impl ProviderKind {
/// stay on the enum for serde and `provider_for_kind`, but they are not
/// first-class catalog rows. Plan is `mode` / base_url; dialect is
/// `wire = openai|anthropic` on the primary provider config.
pub const ALL: [Self; 39] = [
pub const ALL: [Self; 40] = [
Self::Deepseek,
Self::NvidiaNim,
Self::Openai,
@@ -258,6 +263,7 @@ impl ProviderKind {
Self::Telecomjs,
Self::ModelstudioTokenPlan,
Self::Google,
Self::Antigravity,
Self::Custom,
];
+2 -2
View File
@@ -4811,9 +4811,9 @@ fn meta_model_api_scopes_both_documented_key_names_to_official_endpoint() {
fn provider_metadata_registry_covers_every_provider_kind_once() {
let providers = provider::all_providers();
// Full registry keeps legacy dialect/plan kinds for provider_for_kind.
assert_eq!(providers.len(), 44);
assert_eq!(providers.len(), 45);
// Catalog surface is one identity per vendor (no dual-wire / plan rows).
assert_eq!(ProviderKind::ALL.len(), 39);
assert_eq!(ProviderKind::ALL.len(), 40);
assert!(ProviderKind::ALL.len() < providers.len());
let mut ids = std::collections::BTreeSet::new();
+363
View File
@@ -0,0 +1,363 @@
//! Read-only Antigravity (`agy`) credential import.
//!
//! Official `agy` (1.1.13) persists its OAuth token inside the Antigravity
//! app's VSCode-style SQLite store `state.vscdb`, table `ItemTable`, key
//! `antigravityUnifiedStateSync.oauthToken`. Codewhale may read that one
//! value from that exact file only after `codewhale auth external-consent`.
//! The database is opened read-only and never written, refreshed, or
//! copied into the process environment. Codewhale-owned
//! `ANTIGRAVITY_API_KEY` and the process's own `AGY_ADC_AUTH` always win
//! over the external file.
use std::io::{Read as _, Seek, SeekFrom};
use std::path::Path;
use anyhow::{Context, Result, bail};
use codewhale_config::{ExternalCredentialReadGrant, ExternalCredentialSource};
/// ItemTable key holding the `agy` OAuth token.
pub const AGY_OAUTH_TOKEN_KEY: &str = "antigravityUnifiedStateSync.oauthToken";
/// Upper bound on the credential store size we are willing to open.
const AGY_STATE_DB_LIMIT: u64 = 64 * 1024 * 1024;
/// Credential resolution order for the Antigravity route.
///
/// 1. Codewhale-owned `ANTIGRAVITY_API_KEY` (config table or environment)
/// 2. The process's own `AGY_ADC_AUTH` (what `agy` itself calls ADC auth)
/// 3. The consented external `state.vscdb` — read-only, never refreshed
#[must_use]
pub fn antigravity_credential_precedence(
owned_api_key: Option<&str>,
process_env: &std::collections::HashMap<String, String>,
grant: Option<&ExternalCredentialReadGrant>,
) -> AntigravityCredential {
if let Some(key) = owned_api_key.filter(|key| !key.trim().is_empty()) {
return AntigravityCredential::OwnedKey(key.trim().to_string());
}
if let Some(adc) = process_env
.get("AGY_ADC_AUTH")
.filter(|value| !value.trim().is_empty())
{
return AntigravityCredential::ProcessEnv(adc.trim().to_string());
}
let Some(grant) = grant else {
return AntigravityCredential::None;
};
match antigravity_oauth_token_from_grant(grant) {
Ok(Some(token)) => AntigravityCredential::ExternalFile(token),
Ok(None) => AntigravityCredential::None,
Err(error) => AntigravityCredential::Error(error.to_string()),
}
}
/// Where the Antigravity credential came from, for display and receipts.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AntigravityCredential {
OwnedKey(String),
ProcessEnv(String),
ExternalFile(String),
None,
Error(String),
}
impl AntigravityCredential {
#[must_use]
pub fn source_label(&self) -> &'static str {
match self {
Self::OwnedKey(_) => "ANTIGRAVITY_API_KEY",
Self::ProcessEnv(_) => "AGY_ADC_AUTH",
Self::ExternalFile(_) => "agy state.vscdb (read-only)",
Self::None | Self::Error(_) => "none",
}
}
}
/// Extract the `agy` OAuth token from a granted `state.vscdb`.
pub(crate) fn antigravity_oauth_token_from_grant(
grant: &ExternalCredentialReadGrant,
) -> Result<Option<String>> {
if grant.source() != ExternalCredentialSource::AgyCli {
bail!(
"Antigravity import requires an agy_cli grant, not {}",
grant.source().as_str()
);
}
let path = grant.path();
// Secure-open the exact granted path first: regular file only, no
// symlink/reparse-point leaf, size-capped before any SQLite parsing.
let mut file = crate::external_credentials::open_external_regular_file(path)?;
let mut header = [0u8; 16];
let read = file.read(&mut header).with_context(|| {
format!(
"reading SQLite header of {}",
codewhale_config::quote_os_path(path)
)
})?;
if read < 16 || header[..15] != *b"SQLite format 3" {
bail!(
"external agy credential file {} is not a SQLite database",
codewhale_config::quote_os_path(path)
);
}
file.seek(SeekFrom::Start(0)).ok();
let metadata = file
.metadata()
.with_context(|| format!("statting {}", codewhale_config::quote_os_path(path)))?;
if metadata.len() > AGY_STATE_DB_LIMIT {
bail!(
"external agy credential store {} exceeds the {} byte safety limit",
codewhale_config::quote_os_path(path),
AGY_STATE_DB_LIMIT
);
}
// Pin the file identity: SQLite reopens the path by name, so hold the
// secure handle open across the query and prove the inode did not move.
let pinned = file_identity(&file);
drop(file);
let value = query_oauth_token(path)?;
let reopened = std::fs::File::open(path)
.ok()
.and_then(|recheck| file_identity_of(&recheck));
if pinned != reopened {
bail!(
"external agy credential store {} changed while being read",
codewhale_config::quote_os_path(path)
);
}
parse_agy_oauth_token_value(value)
}
#[cfg(unix)]
fn file_identity(file: &std::fs::File) -> Option<(u64, u64)> {
use std::os::unix::fs::MetadataExt as _;
file.metadata().ok().map(|m| (m.dev(), m.ino()))
}
#[cfg(unix)]
fn file_identity_of(file: &std::fs::File) -> Option<(u64, u64)> {
file_identity(file)
}
#[cfg(windows)]
fn file_identity(file: &std::fs::File) -> Option<(u64, u64)> {
use std::os::windows::io::AsRawHandle as _;
use std::os::windows::raw::HANDLE;
// Windows: BY_HANDLE_FILE_INFORMATION via winapi is out of scope here;
// the secure-open layer already rejects reparse points, and the file is
// held by SQLite for the duration of the read. Identity recheck is a
// Unix hardening bonus.
let _ = (file, file.as_raw_handle() as HANDLE);
None
}
#[cfg(windows)]
fn file_identity_of(file: &std::fs::File) -> Option<(u64, u64)> {
let _ = file;
None
}
#[cfg(not(any(unix, windows)))]
fn file_identity(_file: &std::fs::File) -> Option<(u64, u64)> {
None
}
#[cfg(not(any(unix, windows)))]
fn file_identity_of(_file: &std::fs::File) -> Option<(u64, u64)> {
None
}
/// Open the granted path read-only through the shared secure boundary.
fn query_oauth_token(path: &Path) -> Result<Option<String>> {
let connection = rusqlite::Connection::open_with_flags(
path,
rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY | rusqlite::OpenFlags::SQLITE_OPEN_NO_MUTEX,
)
.with_context(|| {
format!(
"opening {} read-only",
codewhale_config::quote_os_path(path)
)
})?;
let value: Option<String> = connection
.query_row(
"SELECT value FROM ItemTable WHERE key = ?1",
[AGY_OAUTH_TOKEN_KEY],
|row| row.get(0),
)
.map(Some)
.or_else(|error| match error {
rusqlite::Error::QueryReturnedNoRows => Ok(None),
other => Err(other),
})
.with_context(|| {
format!(
"querying {} for {AGY_OAUTH_TOKEN_KEY}",
codewhale_config::quote_os_path(path)
)
})?;
Ok(value)
}
/// The stored value is opaque to Codewhale. Accept only shapes observed in
/// the official store — a bare token string or a JSON object with a token
/// member — and never synthesize or trim secrets beyond whitespace.
pub(crate) fn parse_agy_oauth_token_value(value: Option<String>) -> Result<Option<String>> {
let Some(raw) = value else {
return Ok(None);
};
let trimmed = raw.trim();
if trimmed.is_empty() {
return Ok(None);
}
if trimmed.starts_with('{') {
let parsed: serde_json::Value = serde_json::from_str(trimmed)
.with_context(|| "agy OAuth token value is malformed JSON")?;
for member in ["access_token", "accessToken", "token"] {
if let Some(token) = parsed.get(member).and_then(|v| v.as_str()) {
if token.trim().is_empty() {
bail!("agy OAuth token member `{member}` is empty");
}
return Ok(Some(token.to_string()));
}
}
bail!("agy OAuth token JSON carries no access token member");
}
Ok(Some(trimmed.to_string()))
}
#[cfg(test)]
mod tests {
use super::*;
use codewhale_config::ExternalCredentialReadGrant;
use std::collections::HashMap;
fn grant_for(path: &Path) -> ExternalCredentialReadGrant {
codewhale_config::ExternalCredentialConsentToml::read_only(
codewhale_config::ProviderKind::Antigravity,
ExternalCredentialSource::AgyCli,
path.to_path_buf(),
)
.read_grant(
codewhale_config::ProviderKind::Antigravity,
ExternalCredentialSource::AgyCli,
path,
)
.expect("test grant")
}
fn fixture_db(token_value: Option<&str>) -> (tempfile::TempDir, std::path::PathBuf) {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("state.vscdb");
let connection = rusqlite::Connection::open(&path).expect("create fixture database");
connection
.execute_batch(
"CREATE TABLE IF NOT EXISTS ItemTable (key TEXT PRIMARY KEY, value BLOB);",
)
.unwrap();
if let Some(value) = token_value {
connection
.execute(
"INSERT INTO ItemTable (key, value) VALUES (?1, ?2)",
rusqlite::params![AGY_OAUTH_TOKEN_KEY, value],
)
.unwrap();
}
connection
.execute(
"INSERT INTO ItemTable (key, value) VALUES ('antigravityAuthStatus', 'signedIn')",
[],
)
.unwrap();
drop(connection);
(dir, path)
}
#[test]
fn extracts_token_from_fixture_state_db() {
let (_dir, path) = fixture_db(Some("ya29.test-token"));
let grant = grant_for(&path);
assert_eq!(
antigravity_oauth_token_from_grant(&grant).unwrap(),
Some("ya29.test-token".to_string())
);
}
#[test]
fn extracts_access_token_member_from_json_value() {
let (_dir, path) = fixture_db(Some(
r#"{"access_token":"ya29.json","scope":"cloud-platform"}"#,
));
let grant = grant_for(&path);
assert_eq!(
antigravity_oauth_token_from_grant(&grant).unwrap(),
Some("ya29.json".to_string())
);
}
#[test]
fn missing_token_row_is_absent_not_an_error() {
let (_dir, path) = fixture_db(None);
let grant = grant_for(&path);
assert_eq!(antigravity_oauth_token_from_grant(&grant).unwrap(), None);
}
#[test]
fn json_without_token_member_fails_closed() {
assert!(parse_agy_oauth_token_value(Some(r#"{"scope":"x"}"#.into())).is_err());
}
#[test]
fn empty_token_fails_or_is_absent() {
assert_eq!(
parse_agy_oauth_token_value(Some(" ".into())).unwrap(),
None
);
assert!(parse_agy_oauth_token_value(Some(r#"{"access_token":""}"#.to_string())).is_err());
}
#[test]
fn wrong_grant_source_is_rejected() {
let (_dir, path) = fixture_db(Some("token"));
let grant = codewhale_config::ExternalCredentialConsentToml::read_only(
codewhale_config::ProviderKind::Antigravity,
ExternalCredentialSource::DshCli,
path.clone(),
)
.read_grant(
codewhale_config::ProviderKind::Antigravity,
ExternalCredentialSource::DshCli,
&path,
)
.expect("test grant");
assert!(antigravity_oauth_token_from_grant(&grant).is_err());
}
#[test]
fn precedence_owned_key_beats_env_beats_file() {
let mut env = HashMap::new();
env.insert(
"AGY_ADC_AUTH".to_string(),
"adc-process-credential".to_string(),
);
let (_dir, path) = fixture_db(Some("file-token"));
let grant = grant_for(&path);
assert_eq!(
antigravity_credential_precedence(Some("owned-key"), &env, Some(&grant)),
AntigravityCredential::OwnedKey("owned-key".to_string())
);
assert_eq!(
antigravity_credential_precedence(None, &env, Some(&grant)),
AntigravityCredential::ProcessEnv("adc-process-credential".to_string())
);
assert_eq!(
antigravity_credential_precedence(None, &HashMap::new(), Some(&grant)),
AntigravityCredential::ExternalFile("file-token".to_string())
);
assert_eq!(
antigravity_credential_precedence(None, &HashMap::new(), None),
AntigravityCredential::None
);
}
}
+16
View File
@@ -1787,6 +1787,19 @@ impl DeepSeekClient {
request: MessageRequest,
stream: bool,
) -> Result<PreparedOutboundRequest> {
// Antigravity is credential-plane only: the agy login can be
// imported read-only with consent, but Google's cloud-code wire
// protocol is not implemented, and pretending it is OpenAI
// compatible would send credentials to a route that cannot serve
// them. Fail closed before any body is built.
if self.api_provider == crate::config::ApiProvider::Antigravity {
anyhow::bail!(
"Antigravity (agy) requests are not implemented yet: Codewhale can import the \
official CLI's login read-only (`codewhale auth external-consent`), but the \
cloud-code wire protocol is unavailable, so no request is sent. Use the \
`google` provider for Gemini models."
);
}
let mut request =
self.bind_request_to_protocol(self.prepare_model_bound_request(request))?;
if self.is_local_ds4_model(&request.model)
@@ -3240,6 +3253,7 @@ pub(super) fn apply_reasoning_effort(
ApiProvider::Xai => {}
ApiProvider::Mistral => {}
ApiProvider::Google => {}
ApiProvider::Antigravity => {}
},
"low" | "minimal" | "medium" | "mid" | "high" | "" => match provider {
// Handled by the shared DeepSeek table above, before this match.
@@ -3350,6 +3364,7 @@ pub(super) fn apply_reasoning_effort(
ApiProvider::Xai => {}
ApiProvider::Mistral => {}
ApiProvider::Google => {}
ApiProvider::Antigravity => {}
},
"xhigh" | "max" | "highest" | "ultracode" => match provider {
// Handled by the shared DeepSeek table above, before this match.
@@ -3437,6 +3452,7 @@ pub(super) fn apply_reasoning_effort(
ApiProvider::Xai => {}
ApiProvider::Mistral => {}
ApiProvider::Google => {}
ApiProvider::Antigravity => {}
},
_ => {}
}
+31 -2
View File
@@ -92,6 +92,10 @@ pub enum ApiProvider {
/// backend, not an OpenAI alias: thought signatures on tool calls are
/// captured and replayed per Google's contract.
Google,
/// Google Antigravity (`agy`). Credential plane only: consent-gated
/// read-only import of the official CLI's login. Sends fail closed
/// until the cloud-code wire protocol is implemented.
Antigravity,
/// Jiangsu Telecom TokenHub — OpenAI-compatible AI gateway.
Telecomjs,
/// Alibaba Cloud Model Studio — Token Plan (OpenAI-compatible Chat Completions).
@@ -321,7 +325,7 @@ impl ApiProvider {
/// `ApiProvider` discriminant → `ProviderKind` lookup.
/// Index 1 is `None` for the legacy `DeepseekCN` variant.
const KIND_LOOKUP: [Option<codewhale_config::ProviderKind>; 45] = [
const KIND_LOOKUP: [Option<codewhale_config::ProviderKind>; 46] = [
Some(codewhale_config::ProviderKind::Deepseek),
None, // DeepseekCN
Some(codewhale_config::ProviderKind::DeepseekAnthropic),
@@ -361,6 +365,7 @@ impl ApiProvider {
Some(codewhale_config::ProviderKind::Xai),
Some(codewhale_config::ProviderKind::Mistral),
Some(codewhale_config::ProviderKind::Google),
Some(codewhale_config::ProviderKind::Antigravity),
Some(codewhale_config::ProviderKind::Telecomjs),
Some(codewhale_config::ProviderKind::ModelstudioTokenPlan),
Some(codewhale_config::ProviderKind::ModelstudioTokenPlanAnthropic),
@@ -370,7 +375,7 @@ impl ApiProvider {
];
/// `ProviderKind` discriminant → `ApiProvider` lookup.
const FROM_KIND_LOOKUP: [Self; 44] = [
const FROM_KIND_LOOKUP: [Self; 45] = [
Self::Deepseek,
Self::DeepseekAnthropic,
Self::NvidiaNim,
@@ -414,6 +419,7 @@ impl ApiProvider {
Self::ModelstudioCodingPlan,
Self::ModelstudioCodingPlanAnthropic,
Self::Google,
Self::Antigravity,
Self::Custom,
];
@@ -1564,6 +1570,9 @@ pub fn model_completion_names_for_provider(provider: ApiProvider) -> Vec<&'stati
"gemini-2.5-pro",
"gemini-2.5-flash",
],
// The cloud-code wire protocol is not implemented; no model is
// advertised for the credential-import-only route.
ApiProvider::Antigravity => Vec::new(),
// Custom endpoints expose no built-in completion names; the user
// supplies their own model id (#1519).
ApiProvider::Custom => Vec::new(),
@@ -3464,6 +3473,8 @@ pub struct ProvidersConfig {
alias = "gemini"
)]
pub google: ProviderConfig,
#[serde(default, alias = "agy")]
pub antigravity: ProviderConfig,
#[serde(
default,
alias = "telecom-js",
@@ -4922,6 +4933,7 @@ impl Config {
ApiProvider::Xai => &providers.xai,
ApiProvider::Mistral => &providers.mistral,
ApiProvider::Google => &providers.google,
ApiProvider::Antigravity => &providers.antigravity,
ApiProvider::Telecomjs => &providers.telecomjs,
ApiProvider::ModelstudioTokenPlan => &providers.modelstudio_token_plan,
ApiProvider::ModelstudioTokenPlanAnthropic => {
@@ -5000,6 +5012,7 @@ impl Config {
ApiProvider::Xai => &mut providers.xai,
ApiProvider::Mistral => &mut providers.mistral,
ApiProvider::Google => &mut providers.google,
ApiProvider::Antigravity => &mut providers.antigravity,
ApiProvider::Telecomjs => &mut providers.telecomjs,
ApiProvider::ModelstudioTokenPlan => &mut providers.modelstudio_token_plan,
ApiProvider::ModelstudioTokenPlanAnthropic => {
@@ -5363,6 +5376,7 @@ impl Config {
ApiProvider::Xai => DEFAULT_XAI_MODEL,
ApiProvider::Mistral => DEFAULT_MISTRAL_MODEL,
ApiProvider::Google => DEFAULT_GOOGLE_MODEL,
ApiProvider::Antigravity => DEFAULT_ANTIGRAVITY_MODEL,
ApiProvider::Telecomjs => DEFAULT_TELECOMJS_MODEL,
ApiProvider::ModelstudioTokenPlan
| ApiProvider::ModelstudioTokenPlanAnthropic
@@ -5483,6 +5497,7 @@ impl Config {
| ApiProvider::Xai
| ApiProvider::Mistral
| ApiProvider::Google
| ApiProvider::Antigravity
| ApiProvider::Telecomjs
| ApiProvider::ModelstudioTokenPlan
| ApiProvider::ModelstudioTokenPlanAnthropic
@@ -5590,6 +5605,7 @@ impl Config {
ApiProvider::Xai => DEFAULT_XAI_BASE_URL,
ApiProvider::Mistral => DEFAULT_MISTRAL_BASE_URL,
ApiProvider::Google => DEFAULT_GOOGLE_BASE_URL,
ApiProvider::Antigravity => DEFAULT_ANTIGRAVITY_BASE_URL,
ApiProvider::Telecomjs => DEFAULT_TELECOMJS_BASE_URL,
ApiProvider::ModelstudioTokenPlan
| ApiProvider::ModelstudioTokenPlanAnthropic
@@ -7161,6 +7177,7 @@ fn provider_env_base_url_override(provider: ApiProvider) -> Option<String> {
ApiProvider::Xai => &["XAI_BASE_URL"],
ApiProvider::Mistral => &["MISTRAL_BASE_URL"],
ApiProvider::Google => &["GOOGLE_BASE_URL", "GEMINI_BASE_URL"],
ApiProvider::Antigravity => &["ANTIGRAVITY_BASE_URL"],
ApiProvider::Telecomjs => &["TELECOMJS_BASE_URL"],
ApiProvider::ModelstudioTokenPlan | ApiProvider::ModelstudioTokenPlanAnthropic => {
&["MODELSTUDIO_TOKEN_PLAN_BASE_URL"]
@@ -7512,6 +7529,13 @@ fn apply_env_overrides_unlocked(config: &mut Config, policy: ConfigEnvironmentPo
.google
.base_url = Some(value);
}
ApiProvider::Antigravity => {
config
.providers
.get_or_insert_with(ProvidersConfig::default)
.antigravity
.base_url = Some(value);
}
ApiProvider::Telecomjs => {
config
.providers
@@ -7874,6 +7898,8 @@ fn apply_env_overrides_unlocked(config: &mut Config, policy: ConfigEnvironmentPo
ApiProvider::Xai => &mut providers.xai,
ApiProvider::Mistral => &mut providers.mistral,
ApiProvider::Google => &mut providers.google,
ApiProvider::Antigravity => &mut providers.antigravity,
ApiProvider::Antigravity => &mut providers.antigravity,
ApiProvider::Telecomjs => &mut providers.telecomjs,
ApiProvider::ModelstudioTokenPlan => &mut providers.modelstudio_token_plan,
ApiProvider::ModelstudioTokenPlanAnthropic => {
@@ -8220,6 +8246,8 @@ fn apply_env_overrides_unlocked(config: &mut Config, policy: ConfigEnvironmentPo
ApiProvider::Xai => &mut providers.xai,
ApiProvider::Mistral => &mut providers.mistral,
ApiProvider::Google => &mut providers.google,
ApiProvider::Antigravity => &mut providers.antigravity,
ApiProvider::Antigravity => &mut providers.antigravity,
ApiProvider::Telecomjs => &mut providers.telecomjs,
ApiProvider::ModelstudioTokenPlan => &mut providers.modelstudio_token_plan,
ApiProvider::ModelstudioTokenPlanAnthropic => {
@@ -9619,6 +9647,7 @@ fn merge_providers(
xai: merge_provider_config(base.xai, override_cfg.xai),
mistral: merge_provider_config(base.mistral, override_cfg.mistral),
google: merge_provider_config(base.google, override_cfg.google),
antigravity: merge_provider_config(base.antigravity, override_cfg.antigravity),
telecomjs: merge_provider_config(base.telecomjs, override_cfg.telecomjs),
modelstudio_token_plan: merge_provider_config(
base.modelstudio_token_plan,
+2
View File
@@ -222,6 +222,8 @@ pub const XAI_GROK_4_20_0309_NON_REASONING_MODEL: &str = "grok-4.20-0309-non-rea
pub const DEFAULT_XAI_BASE_URL: &str = "https://api.x.ai/v1";
pub const DEFAULT_MISTRAL_MODEL: &str = "mistral-code-latest";
pub const DEFAULT_MISTRAL_BASE_URL: &str = "https://api.mistral.ai/v1";
pub const DEFAULT_ANTIGRAVITY_MODEL: &str = "gemini-3-pro-preview";
pub const DEFAULT_ANTIGRAVITY_BASE_URL: &str = "https://cloudcode-pa.googleapis.com/v1internal";
pub const DEFAULT_GOOGLE_MODEL: &str = "gemini-3.1-pro-preview";
pub const DEFAULT_GOOGLE_BASE_URL: &str =
"https://generativelanguage.googleapis.com/v1beta/openai/";
+1
View File
@@ -315,6 +315,7 @@ fn provider_base_url_table_key(provider: ApiProvider) -> anyhow::Result<&'static
ApiProvider::Xai => Ok("xai"),
ApiProvider::Mistral => Ok("mistral"),
ApiProvider::Google => Ok("google"),
ApiProvider::Antigravity => Ok("antigravity"),
ApiProvider::Telecomjs => Ok("telecomjs"),
ApiProvider::ModelstudioTokenPlan => Ok("modelstudio_token_plan"),
ApiProvider::ModelstudioTokenPlanAnthropic => Ok("modelstudio_token_plan_anthropic"),
+12
View File
@@ -94,6 +94,18 @@ pub(crate) fn read_to_string(grant: &ExternalCredentialReadGrant) -> Result<Opti
Ok(Some(contents))
}
/// Open the exact granted file through the secure boundary (regular file
/// only, no symlink/reparse leaf) without requiring UTF-8. Used by binary
/// credential stores such as the Antigravity `state.vscdb` reader.
pub(crate) fn open_external_regular_file(path: &Path) -> Result<std::fs::File> {
open_secure_regular_file(path, false).with_context(|| {
format!(
"securely opening external credential file {}",
codewhale_config::quote_os_path(path)
)
})
}
/// Read one Codewhale-owned credential file through the same no-follow,
/// bounded I/O boundary used for external grants. On Unix the opened handle
/// must belong to the effective user and have no group/other permission bits.
+1
View File
@@ -21,6 +21,7 @@ use rust_i18n::i18n;
i18n!("locales", fallback = ["en"]);
mod acp_server;
mod agy_credentials;
mod artifacts;
mod audit;
mod auto_reasoning;
+1
View File
@@ -874,6 +874,7 @@ pub(crate) fn mirror_saved_api_key_in_config(
ApiProvider::Xai => &mut providers.xai,
ApiProvider::Mistral => &mut providers.mistral,
ApiProvider::Google => &mut providers.google,
ApiProvider::Antigravity => &mut providers.antigravity,
ApiProvider::Telecomjs => &mut providers.telecomjs,
ApiProvider::ModelstudioTokenPlan => &mut providers.modelstudio_token_plan,
ApiProvider::ModelstudioTokenPlanAnthropic => {
+3
View File
@@ -132,6 +132,7 @@ the listed provider env vars.
| `xai` | `[providers.xai]` | OpenAI Chat Completions | `XAI_API_KEY` |
| `mistral` | `[providers.mistral]` | OpenAI Chat Completions | `MISTRAL_API_KEY` |
| `google` | `[providers.google]` | OpenAI Chat Completions (official Gemini OpenAI-compat route; captures and replays thought signatures on tool calls) | `GOOGLE_API_KEY`, `GEMINI_API_KEY` |
| `antigravity` | `[providers.antigravity]` | none — requests fail closed; credential import only | `ANTIGRAVITY_API_KEY` (key plane); `AGY_ADC_AUTH` (process env) |
| `modelstudio-token-plan` | `[providers.modelstudio_token_plan]` | OpenAI Chat Completions | `MODELSTUDIO_API_KEY`, `DASHSCOPE_API_KEY` |
| `modelstudio-token-plan-anthropic` | `[providers.modelstudio_token_plan_anthropic]` | Anthropic Messages | `MODELSTUDIO_API_KEY`, `DASHSCOPE_API_KEY` |
| `modelstudio-coding-plan` | `[providers.modelstudio_coding_plan]` | OpenAI Chat Completions | `MODELSTUDIO_API_KEY`, `DASHSCOPE_API_KEY` |
@@ -455,6 +456,7 @@ configuration path instead of guessing a vendor page.
| `xai` | [xAI Console](https://console.x.ai/) for an API key, Codewhale-owned device login, or explicitly consented read-only Grok CLI credentials. |
| `mistral` | [Mistral Console (la Plateforme)](https://console.mistral.ai/api-keys) |
| `google` | [Google AI Studio](https://aistudio.google.com/apikey) — Codewhale uses the official Gemini OpenAI-compatible endpoint and never reads Google OAuth files. |
| `antigravity` | Sign in with the official `agy` CLI (1.1.13). Codewhale can read that login's OAuth token read-only from the exact pinned `state.vscdb` after `codewhale auth external-consent`; it never writes or refreshes the file. An `ANTIGRAVITY_API_KEY` or the process's `AGY_ADC_AUTH` wins over the file. The cloud-code wire protocol is not implemented: requests fail closed with an actionable message — use `google` for Gemini models. |
| `modelstudio-token-plan`, `modelstudio-token-plan-anthropic`, `modelstudio-coding-plan`, `modelstudio-coding-plan-anthropic` | [Alibaba Cloud Model Studio (Bailian console)](https://bailian.console.aliyun.com/) — create or copy a Model Studio API key. |
| `custom` | Set the named provider's `base_url` and `api_key_env` or `api_key`; no canonical vendor credential page exists. |
@@ -523,6 +525,7 @@ Kimi remains API-key-only; external consent for Kimi is rejected.
| `siliconflow-CN` | `[providers.siliconflow_cn]` | `SILICONFLOW_API_KEY` | `SILICONFLOW_BASE_URL`; default `https://api.siliconflow.cn/v1` | Uses the SiliconFlow model set | China regional SiliconFlow route. Falls back to `[providers.siliconflow]` for api_key / base_url / model when unset. Select it with `provider = "siliconflow-CN"` or `CODEWHALE_PROVIDER=siliconflow-CN`. |
| `arcee` | `[providers.arcee]` | `ARCEE_API_KEY` | `ARCEE_BASE_URL`; default `https://api.arcee.ai/api/v1` | `trinity-large-thinking`, `trinity-large-preview` | Arcee AI direct OpenAI-compatible route, tracked as 256K-context BF16 serving. `ARCEE_MODEL` is accepted. OpenRouter's `arcee-ai/trinity-large-thinking` remains the OpenRouter namespaced model ID; direct Arcee uses the bare `trinity-large-thinking` ID. |
| `moonshot` | `[providers.moonshot]` | `MOONSHOT_API_KEY`, `KIMI_API_KEY` | `MOONSHOT_BASE_URL`, `KIMI_BASE_URL`; default `https://api.moonshot.ai/v1` | Direct Moonshot: `kimi-k3`, `kimi-k2.7-code`, `kimi-k2.6`; Kimi Code membership: `k3`, `kimi-for-coding`, `kimi-for-coding-highspeed` at `https://api.kimi.com/coding/v1` | Moonshot/Kimi route. `kimi` and `kimi-k2` aliases select `kimi-k2.7-code`; `MOONSHOT_MODEL`, `KIMI_MODEL_NAME`, and `KIMI_MODEL` are accepted. Kimi thinking streams through `reasoning_content`; Codewhale keeps it in Thinking cells and replays it for thinking/tool-call continuity. For direct K3, use exact `base_url = "https://api.moonshot.ai/v1"` and `model = "kimi-k3"`; it is always-thinking and receives top-level `reasoning_effort = "low" | "high" | "max"` (`off` normalizes to `low`), uses only `max_completion_tokens`, and omits `temperature`/`top_p` per the [K3 quickstart](https://platform.kimi.ai/docs/guide/kimi-k3-quickstart). For Kimi Code K3, use a key from the [Kimi Code console](https://www.kimi.com/code/console), exact `base_url = "https://api.kimi.com/coding/v1"`, and bare `model = "k3"`; `off` becomes enabled `low`, while normal dispatched `auto` selects and sends a concrete Codewhale tier. Only an omitted reasoning setting leaves the provider default in control. That membership route defaults safely to 262,144 context tokens; the [Kimi Code model-tier table](https://www.kimi.com/code/docs/en/kimi-code/models.html) grants Allegretto and higher plans up to 1M, which those plans may express as `context_window = 1048576`. `k3[1m]` is Claude Code-only and Codewhale rejects it. `kimi-for-coding` remains the valid K2.7 membership route, and `kimi-for-coding-highspeed` is its own high-speed roster entry (262,144 context); membership ids are rejected on the direct platform endpoint, and `kimi-k3` stays rejected on the membership endpoint. Billing is decided by the endpoint the route resolves to, judged once against the two exact product endpoints: direct Moonshot (`https://api.moonshot.ai/v1` or the default) bills metered with dollar estimates, the exact Kimi Code membership endpoint bills as Kimi Code quota and never shows dollar estimates, and anything else — a gateway host, a neighboring Kimi-hosted path — reports `cost: unknown` rather than borrowing either product. An imported Kimi Code token with no `base_url` in its table still resolves to the membership endpoint, so it bills as Kimi Code quota and never accrues dollars. A completed turn, parent or sub-agent, is billed from the immutable endpoint receipt its own client was built with, never from a later config re-read: `MOONSHOT_BASE_URL`/`KIMI_BASE_URL` are merged into the *active* provider's table only, and an in-turn provider switch can move the ambient config off the route that actually ran. Legacy `auth_mode = "kimi_oauth"` fails to API-key guidance without probing Kimi CLI files. Codewhale does not impersonate `kimi_cli` or `kimi_code_cli`. **China-region keys:** contributor field evidence (@vFONGv, PR #5229, verified on Windows 10) reports that a China-region Moonshot key must be paired with `base_url = "https://api.moonshot.cn/v1"`; left on the default international host (`https://api.moonshot.ai/v1`) it fails authentication. We have no China-region key to verify this ourselves, so it is recorded as a user report rather than a tested route. Note also that editing `base_url` alone does not take effect until `codewhale auth set` is re-run for that provider. |
| `antigravity` | `[providers.antigravity]` | `ANTIGRAVITY_API_KEY` | `ANTIGRAVITY_BASE_URL`; default `https://cloudcode-pa.googleapis.com/v1internal` | none advertised — requests fail closed until the cloud-code wire protocol exists | Antigravity (`agy` 1.1.13) credential plane: consent-gated read-only import of the official CLI's `state.vscdb` OAuth token (`antigravityUnifiedStateSync.oauthToken`), pinned to the exact per-OS app-profile path. The store is opened read-only through the secure no-follow boundary with an inode recheck; Codewhale never writes, refreshes, or re-authenticates. Precedence: `ANTIGRAVITY_API_KEY` > process `AGY_ADC_AUTH` > consented file. Not an embed of any other harness. No live calls made in this environment. |
| `google` | `[providers.google]` | `GOOGLE_API_KEY`, `GEMINI_API_KEY` | `GOOGLE_BASE_URL`, `GEMINI_BASE_URL`; default `https://generativelanguage.googleapis.com/v1beta/openai/` | `gemini-3.1-pro-preview` (default); `/model` also lists `gemini-3-pro-preview`, `gemini-3.6-flash`, `gemini-3.5-flash`, `gemini-3.5-flash-lite`, `gemini-2.5-pro`, `gemini-2.5-flash` | Google Gemini as its own backend on the official OpenAI-compatible Chat Completions route. Thinking models capture `extra_content.google.thought_signature` on tool calls and replay it with the assistant tool-call messages; replaying a tool call whose signature was not captured fails closed with an actionable error instead of letting the tool loop break. `gemini-2.5-flash-lite` ships thinking off and degrades with a warning instead. Reasoning effort maps onto the documented `google.thinking_config.thinking_level` (`low`/`high`). The dialect binds to the exact official base URL: a `google` row pointed at another gateway gets plain OpenAI semantics and no signature requirements. Codewhale never reads Google OAuth files; only an AI Studio API key is used. Not live-tested against the real endpoint in this environment. |
| `zai` | `[providers.zai]` | `ZAI_API_KEY`, `Z_AI_API_KEY` | `ZAI_BASE_URL`, `Z_AI_BASE_URL`; default `https://api.z.ai/api/coding/paas/v4`; general API `https://api.z.ai/api/paas/v4` | `GLM-5.2` default; `/model` also lists `GLM-5.3`, `GLM-5.1`, and `GLM-5-Turbo` | Z.AI GLM Coding Plan route. `GLM-5.2` stays the default. `GLM-5.3` is a first-class picker row (`model = "GLM-5.3"` or `ZAI_MODEL=GLM-5.3`) and is not rewritten to another vendor. Limits and reasoning options are inherited from `GLM-5.2` until Z.ai publishes distinct 5.3 metadata; it carries no price. A live call can still 429 with entitlement code 1311 on accounts that are not provisioned for 5.3. |
| `stepfun` | `[providers.stepfun]` | `STEPFUN_API_KEY`, `STEP_API_KEY` | `STEPFUN_BASE_URL`, `STEP_BASE_URL`; default `https://api.stepfun.ai/v1`; Coding Plan endpoint `https://api.stepfun.ai/step_plan/v1` | `step-3.7-flash` | StepFun / StepFlash direct OpenAI-compatible route. `/provider` setup asks which billing route the key belongs to — pay-as-you-go or Step Plan — validates the key against the chosen endpoint, and writes the answer to `[providers.stepfun].base_url` only. A base URL that is neither recognized route is left alone and the question is skipped. You can also set `[providers.stepfun].base_url` or `STEP_BASE_URL` to the Coding Plan URL by hand. Offline accounting labels recognized routes as `stepfun-payg` or `stepfun-plan` without persisting the raw endpoint, and only the standard PAYG route receives token pricing. `STEPFUN_MODEL` and `STEP_MODEL` are accepted. |