mirror of
https://github.com/codewhale-hq/Codewhale.git
synced 2026-11-11 01:51:34 -10:00
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:
1 parent
0889eb53cc
commit
531b64ed9d
16 files changed
+612
-13
No files matched your search
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
];
|
||||
|
||||
|
||||
@@ -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";
|
||||
@@ -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,
|
||||
];
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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 => {}
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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/";
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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. |
|
||||
|
||||
Reference in new issue
Block a user