docs/security/ANTIGRAVITY_AUTH.fr.md
Retour au README
Antigravity (Google Cloud Code Assist) est un fournisseur de modèles IA soutenu par Google qui offre l'accès à des modèles tels que Claude Opus 4.6 et Gemini via l'infrastructure cloud de Google. Ce document fournit un guide complet sur le fonctionnement de l'authentification, la récupération des modèles et l'implémentation d'un nouveau fournisseur dans PicoClaw.
Antigravity utilise OAuth 2.0 avec PKCE (Proof Key for Code Exchange) pour une authentification sécurisée :
┌─────────────┐ ┌─────────────────┐
│ Client │ ───(1) Generate PKCE Pair────────> │ │
│ │ ───(2) Open Auth URL─────────────> │ Google OAuth │
│ │ │ Server │
│ │ <──(3) Redirect with Code───────── │ │
│ │ └─────────────────┘
│ │ ───(4) Exchange Code for Tokens──> │ Token URL │
│ │ │ │
│ │ <──(5) Access + Refresh Tokens──── │ │
└─────────────┘ └─────────────────┘
function generatePkce(): { verifier: string; challenge: string } {
const verifier = randomBytes(32).toString("hex");
const challenge = createHash("sha256").update(verifier).digest("base64url");
return { verifier, challenge };
}
const AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
const REDIRECT_URI = "http://localhost:51121/oauth-callback";
function buildAuthUrl(params: { challenge: string; state: string }): string {
const url = new URL(AUTH_URL);
url.searchParams.set("client_id", CLIENT_ID);
url.searchParams.set("response_type", "code");
url.searchParams.set("redirect_uri", REDIRECT_URI);
url.searchParams.set("scope", SCOPES.join(" "));
url.searchParams.set("code_challenge", params.challenge);
url.searchParams.set("code_challenge_method", "S256");
url.searchParams.set("state", params.state);
url.searchParams.set("access_type", "offline");
url.searchParams.set("prompt", "consent");
return url.toString();
}
Portées requises :
const SCOPES = [
"https://www.googleapis.com/auth/cloud-platform",
"https://www.googleapis.com/auth/userinfo.email",
"https://www.googleapis.com/auth/userinfo.profile",
"https://www.googleapis.com/auth/cclog",
"https://www.googleapis.com/auth/experimentsandconfigs",
];
Mode automatique (développement local) :
Mode manuel (distant/sans interface graphique) :
const TOKEN_URL = "https://oauth2.googleapis.com/token";
async function exchangeCode(params: {
code: string;
verifier: string;
}): Promise<{ access: string; refresh: string; expires: number }> {
const response = await fetch(TOKEN_URL, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
code: params.code,
grant_type: "authorization_code",
redirect_uri: REDIRECT_URI,
code_verifier: params.verifier,
}),
});
const data = await response.json();
return {
access: data.access_token,
refresh: data.refresh_token,
expires: Date.now() + data.expires_in * 1000 - 5 * 60 * 1000, // 5 min buffer
};
}
E-mail de l'utilisateur :
async function fetchUserEmail(accessToken: string): Promise<string | undefined> {
const response = await fetch(
"https://www.googleapis.com/oauth2/v1/userinfo?alt=json",
{ headers: { Authorization: `Bearer ${accessToken}` } }
);
const data = await response.json();
return data.email;
}
ID du projet (requis pour les appels API) :
async function fetchProjectId(accessToken: string): Promise<string> {
const headers = {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
"User-Agent": "google-api-nodejs-client/9.15.1",
"X-Goog-Api-Client": "google-cloud-sdk vscode_cloudshelleditor/0.1",
"Client-Metadata": JSON.stringify({
ideType: "IDE_UNSPECIFIED",
platform: "PLATFORM_UNSPECIFIED",
pluginType: "GEMINI",
}),
};
const response = await fetch(
"https://cloudcode-pa.googleapis.com/v1internal:loadCodeAssist",
{
method: "POST",
headers,
body: JSON.stringify({
metadata: {
ideType: "IDE_UNSPECIFIED",
platform: "PLATFORM_UNSPECIFIED",
pluginType: "GEMINI",
},
}),
}
);
const data = await response.json();
return data.cloudaicompanionProject || "rising-fact-p41fc"; // Valeur par défaut
}
Important : Ceux-ci sont encodés en base64 dans le code source pour la synchronisation avec pi-ai :
const decode = (s: string) => Buffer.from(s, "base64").toString();
const CLIENT_ID = decode(
"MTA3MTAwNjA2MDU5MS10bWhzc2luMmgyMWxjcmUyMzV2dG9sb2poNGc0MDNlcC5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbQ=="
);
const CLIENT_SECRET = decode("R09DU1BYLUs1OEZXUjQ4NkxkTEoxbUxCOHNYQzR6NnFEQWY=");
Flux automatique (machines locales avec navigateur) :
Flux manuel (distant/sans interface/WSL2) :
function shouldUseManualOAuthFlow(isRemote: boolean): boolean {
return isRemote || isWSL2Sync();
}
type OAuthCredential = {
type: "oauth";
provider: "google-antigravity";
access: string; // Jeton d'accès
refresh: string; // Jeton de rafraîchissement
expires: number; // Horodatage d'expiration (ms depuis epoch)
email?: string; // E-mail de l'utilisateur
projectId?: string; // ID du projet Google Cloud
};
Les identifiants incluent un jeton de rafraîchissement qui peut être utilisé pour obtenir de nouveaux jetons d'accès lorsque le jeton actuel expire. L'expiration est définie avec un tampon de 5 minutes pour éviter les conditions de concurrence.
const BASE_URL = "https://cloudcode-pa.googleapis.com";
async function fetchAvailableModels(
accessToken: string,
projectId: string
): Promise<Model[]> {
const headers = {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
"User-Agent": "antigravity",
"X-Goog-Api-Client": "google-cloud-sdk vscode_cloudshelleditor/0.1",
};
const response = await fetch(
`${BASE_URL}/v1internal:fetchAvailableModels`,
{
method: "POST",
headers,
body: JSON.stringify({ project: projectId }),
}
);
const data = await response.json();
// Retourne les modèles avec les informations de quota
return Object.entries(data.models).map(([modelId, modelInfo]) => ({
id: modelId,
displayName: modelInfo.displayName,
quotaInfo: {
remainingFraction: modelInfo.quotaInfo?.remainingFraction,
resetTime: modelInfo.quotaInfo?.resetTime,
isExhausted: modelInfo.quotaInfo?.isExhausted,
},
}));
}
type FetchAvailableModelsResponse = {
models?: Record<string, {
displayName?: string;
quotaInfo?: {
remainingFraction?: number | string;
resetTime?: string; // Horodatage ISO 8601
isExhausted?: boolean;
};
}>;
};
export async function fetchAntigravityUsage(
token: string,
timeoutMs: number
): Promise<ProviderUsageSnapshot> {
// 1. Récupérer les crédits et les informations du plan
const loadCodeAssistRes = await fetch(
`${BASE_URL}/v1internal:loadCodeAssist`,
{
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
metadata: {
ideType: "ANTIGRAVITY",
platform: "PLATFORM_UNSPECIFIED",
pluginType: "GEMINI",
},
}),
}
);
// Extraire les informations de crédits
const { availablePromptCredits, planInfo, currentTier } = data;
// 2. Récupérer les quotas des modèles
const modelsRes = await fetch(
`${BASE_URL}/v1internal:fetchAvailableModels`,
{
method: "POST",
headers: { Authorization: `Bearer ${token}` },
body: JSON.stringify({ project: projectId }),
}
);
// Construire les fenêtres d'utilisation
return {
provider: "google-antigravity",
displayName: "Google Antigravity",
windows: [
{ label: "Credits", usedPercent: calculateUsedPercent(available, monthly) },
// Quotas individuels des modèles...
],
plan: currentTier?.name || planType,
};
}
type ProviderUsageSnapshot = {
provider: "google-antigravity";
displayName: string;
windows: UsageWindow[];
plan?: string;
error?: string;
};
type UsageWindow = {
label: string; // "Credits" ou ID du modèle
usedPercent: number; // 0-100
resetAt?: number; // Horodatage de réinitialisation du quota
};
const antigravityPlugin = {
id: "google-antigravity-auth",
name: "Google Antigravity Auth",
description: "OAuth flow for Google Antigravity (Cloud Code Assist)",
configSchema: emptyPluginConfigSchema(),
register(api: PicoClawPluginApi) {
api.registerProvider({
id: "google-antigravity",
label: "Google Antigravity",
docsPath: "/providers/models",
aliases: ["antigravity"],
auth: [
{
id: "oauth",
label: "Google OAuth",
hint: "PKCE + localhost callback",
kind: "oauth",
run: async (ctx: ProviderAuthContext) => {
// Implémentation OAuth ici
},
},
],
});
},
};
type ProviderAuthContext = {
config: PicoClawConfig;
agentDir?: string;
workspaceDir?: string;
prompter: WizardPrompter; // Invites/notifications UI
runtime: RuntimeEnv; // Journalisation, etc.
isRemote: boolean; // Exécution à distance ou non
openUrl: (url: string) => Promise<void>; // Ouverture du navigateur
oauth: {
createVpsAwareHandlers: Function;
};
};
type ProviderAuthResult = {
profiles: Array<{
profileId: string;
credential: AuthProfileCredential;
}>;
configPatch?: Partial<PicoClawConfig>;
defaultModel?: string;
notes?: string[];
};
pkg/providers/ et pkg/auth/)crypto et net/httpconst REQUIRED_HEADERS = {
"Authorization": `Bearer ${accessToken}`,
"Content-Type": "application/json",
"User-Agent": "antigravity", // ou "google-api-nodejs-client/9.15.1"
"X-Goog-Api-Client": "google-cloud-sdk vscode_cloudshelleditor/0.1",
};
// Pour les appels loadCodeAssist, inclure également :
const CLIENT_METADATA = {
ideType: "ANTIGRAVITY", // ou "IDE_UNSPECIFIED"
platform: "PLATFORM_UNSPECIFIED",
pluginType: "GEMINI",
};
Antigravity utilise des modèles compatibles Gemini, les schémas d'outils doivent donc être assainis :
const GOOGLE_SCHEMA_UNSUPPORTED_KEYWORDS = new Set([
"patternProperties",
"additionalProperties",
"$schema",
"$id",
"$ref",
"$defs",
"definitions",
"examples",
"minLength",
"maxLength",
"minimum",
"maximum",
"multipleOf",
"pattern",
"format",
"minItems",
"maxItems",
"uniqueItems",
"minProperties",
"maxProperties",
]);
// Nettoyer le schéma avant l'envoi
function cleanToolSchemaForGemini(schema: Record<string, unknown>): unknown {
// Supprimer les mots-clés non supportés
// S'assurer que le niveau supérieur a type: "object"
// Aplatir les unions anyOf/oneOf
}
Pour les modèles Claude via Antigravity, les blocs de réflexion nécessitent un traitement spécial :
const ANTIGRAVITY_SIGNATURE_RE = /^[A-Za-z0-9+/]+={0,2}$/;
export function sanitizeAntigravityThinkingBlocks(
messages: AgentMessage[]
): AgentMessage[] {
// Valider les signatures de réflexion
// Normaliser les champs de signature
// Rejeter les blocs de réflexion non signés
}
| Point de terminaison | Méthode | Objectif |
|---|---|---|
https://accounts.google.com/o/oauth2/v2/auth | GET | Autorisation OAuth |
https://oauth2.googleapis.com/token | POST | Échange de jetons |
https://www.googleapis.com/oauth2/v1/userinfo | GET | Informations utilisateur (e-mail) |
| Point de terminaison | Méthode | Objectif |
|---|---|---|
https://cloudcode-pa.googleapis.com/v1internal:loadCodeAssist | POST | Charger les infos du projet, crédits, plan |
https://cloudcode-pa.googleapis.com/v1internal:fetchAvailableModels | POST | Lister les modèles disponibles avec quotas |
https://cloudcode-pa.googleapis.com/v1internal:streamGenerateContent?alt=sse | POST | Point de terminaison de streaming de chat |
Format de requête API (chat) :
Le point de terminaison v1internal:streamGenerateContent attend une enveloppe encapsulant la requête Gemini standard :
{
"project": "your-project-id",
"model": "model-id",
"request": {
"contents": [...],
"systemInstruction": {...},
"generationConfig": {...},
"tools": [...]
},
"requestType": "agent",
"userAgent": "antigravity",
"requestId": "agent-timestamp-random"
}
Format de réponse API (SSE) :
Chaque message SSE (data: {...}) est encapsulé dans un champ response :
{
"response": {
"candidates": [...],
"usageMetadata": {...},
"modelVersion": "...",
"responseId": "..."
},
"traceId": "...",
"metadata": {}
}
{
"model_list": [
{
"model_name": "gemini-flash",
"model": "antigravity/gemini-3-flash",
"auth_method": "oauth"
}
],
"agents": {
"defaults": {
"model_name": "gemini-flash"
}
}
}
Les profils d'authentification sont stockés dans ~/.picoclaw/auth.json :
{
"credentials": {
"google-antigravity": {
"access_token": "ya29...",
"refresh_token": "1//...",
"expires_at": "2026-01-01T00:00:00Z",
"provider": "google-antigravity",
"auth_method": "oauth",
"email": "[email protected]",
"project_id": "my-project-id"
}
}
}
Les fournisseurs PicoClaw sont implémentés en tant que packages Go sous pkg/providers/. Pour ajouter un nouveau fournisseur :
Créez un nouveau fichier Go dans pkg/providers/ :
pkg/providers/
└── your_provider.go
Votre fournisseur doit implémenter l'interface Provider définie dans pkg/providers/types.go :
package providers
type YourProvider struct {
apiKey string
apiBase string
}
func NewYourProvider(apiKey, apiBase, proxy string) *YourProvider {
if apiBase == "" {
apiBase = "https://api.your-provider.com/v1"
}
return &YourProvider{apiKey: apiKey, apiBase: apiBase}
}
func (p *YourProvider) Chat(ctx context.Context, messages []Message, tools []Tool, cb StreamCallback) error {
// Implémenter la complétion de chat avec streaming
}
Ajoutez votre fournisseur au switch de protocole dans pkg/providers/factory.go :
case "your-provider":
return NewYourProvider(sel.apiKey, sel.apiBase, sel.proxy), nil
Ajoutez une entrée par défaut dans pkg/config/defaults.go :
{
ModelName: "your-model",
Model: "your-provider/model-name",
APIKey: "",
},
Si votre fournisseur nécessite OAuth ou une authentification spéciale, ajoutez un cas dans cmd/picoclaw/internal/auth/helpers.go :
case "your-provider":
authLoginYourProvider()
config.json{
"model_list": [
{
"model_name": "your-model",
"model": "your-provider/model-name",
"api_key": "your-api-key",
"api_base": "https://api.your-provider.com/v1"
}
]
}
# S'authentifier avec un fournisseur
picoclaw auth login --provider your-provider
# Lister les modèles (pour Antigravity)
picoclaw auth models
# Démarrer la passerelle
picoclaw gateway
# Exécuter un agent avec un modèle spécifique
picoclaw agent -m "Hello" --model your-model
# Remplacer le modèle par défaut
export PICOCLAW_AGENTS_DEFAULTS_MODEL=your-model
# Remplacer les paramètres du fournisseur
export PICOCLAW_MODEL_LIST='[{"model_name":"your-model","model":"your-provider/model-name","api_key":"..."}]'
Fichiers source :
pkg/providers/antigravity_provider.go - Implémentation du fournisseur Antigravitypkg/auth/oauth.go - Implémentation du flux OAuthpkg/auth/store.go - Stockage des identifiants d'authentification (~/.picoclaw/auth.json)pkg/providers/factory.go - Factory des fournisseurs et routage de protocolepkg/providers/types.go - Définitions de l'interface fournisseurcmd/picoclaw/internal/auth/helpers.go - Commandes CLI d'authentificationDocumentation :
docs/ANTIGRAVITY_USAGE.md - Guide d'utilisation d'Antigravitydocs/migration/model-list-migration.md - Guide de migrationAntigravity retourne une erreur 429 lorsque les quotas du projet/modèle sont épuisés. La réponse d'erreur contient souvent un quotaResetDelay dans le champ details.
Exemple d'erreur 429 :
{
"error": {
"code": 429,
"message": "You have exhausted your capacity on this model. Your quota will reset after 4h30m28s.",
"status": "RESOURCE_EXHAUSTED",
"details": [
{
"@type": "type.googleapis.com/google.rpc.ErrorInfo",
"metadata": {
"quotaResetDelay": "4h30m28.060903746s"
}
}
]
}
}
Certains modèles peuvent apparaître dans la liste des modèles disponibles mais retourner une réponse vide (200 OK mais flux SSE vide). Cela se produit généralement pour les modèles en préversion ou restreints que le projet actuel n'a pas la permission d'utiliser.
Traitement : Traiter les réponses vides comme des erreurs informant l'utilisateur que le modèle pourrait être restreint ou invalide pour son projet.
picoclaw auth login --provider antigravity~/.picoclaw/auth.jsonpicoclaw auth login --provider antigravity