mirror of
https://github.com/sipeed/picoclaw.git
synced 2026-06-12 18:08:54 +00:00
refactor(docs): reorganize docs by type and locale
This commit is contained in:
@@ -0,0 +1,809 @@
|
||||
> Retour au [README](../project/README.fr.md)
|
||||
|
||||
# Guide d'authentification et d'intégration Antigravity
|
||||
|
||||
## Aperçu
|
||||
|
||||
**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.
|
||||
|
||||
---
|
||||
|
||||
## Table des matières
|
||||
|
||||
1. [Flux d'authentification](#flux-dauthentification)
|
||||
2. [Détails de l'implémentation OAuth](#détails-de-limplémentation-oauth)
|
||||
3. [Gestion des jetons](#gestion-des-jetons)
|
||||
4. [Récupération de la liste des modèles](#récupération-de-la-liste-des-modèles)
|
||||
5. [Suivi de l'utilisation](#suivi-de-lutilisation)
|
||||
6. [Structure du plugin fournisseur](#structure-du-plugin-fournisseur)
|
||||
7. [Exigences d'intégration](#exigences-dintégration)
|
||||
8. [Points de terminaison API](#points-de-terminaison-api)
|
||||
9. [Configuration](#configuration)
|
||||
10. [Créer un nouveau fournisseur dans PicoClaw](#créer-un-nouveau-fournisseur-dans-picoclaw)
|
||||
|
||||
---
|
||||
|
||||
## Flux d'authentification
|
||||
|
||||
### 1. OAuth 2.0 avec PKCE
|
||||
|
||||
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──── │ │
|
||||
└─────────────┘ └─────────────────┘
|
||||
```
|
||||
|
||||
### 2. Étapes détaillées
|
||||
|
||||
#### Étape 1 : Générer les paramètres PKCE
|
||||
```typescript
|
||||
function generatePkce(): { verifier: string; challenge: string } {
|
||||
const verifier = randomBytes(32).toString("hex");
|
||||
const challenge = createHash("sha256").update(verifier).digest("base64url");
|
||||
return { verifier, challenge };
|
||||
}
|
||||
```
|
||||
|
||||
#### Étape 2 : Construire l'URL d'autorisation
|
||||
```typescript
|
||||
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 :**
|
||||
```typescript
|
||||
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",
|
||||
];
|
||||
```
|
||||
|
||||
#### Étape 3 : Gérer le callback OAuth
|
||||
|
||||
**Mode automatique (développement local) :**
|
||||
- Démarrer un serveur HTTP local sur le port 51121
|
||||
- Attendre la redirection de Google
|
||||
- Extraire le code d'autorisation des paramètres de requête
|
||||
|
||||
**Mode manuel (distant/sans interface graphique) :**
|
||||
- Afficher l'URL d'autorisation à l'utilisateur
|
||||
- L'utilisateur complète l'authentification dans son navigateur
|
||||
- L'utilisateur colle l'URL de redirection complète dans le terminal
|
||||
- Analyser le code depuis l'URL collée
|
||||
|
||||
#### Étape 4 : Échanger le code contre des jetons
|
||||
```typescript
|
||||
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
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
#### Étape 5 : Récupérer les données utilisateur supplémentaires
|
||||
|
||||
**E-mail de l'utilisateur :**
|
||||
```typescript
|
||||
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) :**
|
||||
```typescript
|
||||
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
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Détails de l'implémentation OAuth
|
||||
|
||||
### Identifiants client
|
||||
|
||||
**Important :** Ceux-ci sont encodés en base64 dans le code source pour la synchronisation avec pi-ai :
|
||||
|
||||
```typescript
|
||||
const decode = (s: string) => Buffer.from(s, "base64").toString();
|
||||
|
||||
const CLIENT_ID = decode(
|
||||
"MTA3MTAwNjA2MDU5MS10bWhzc2luMmgyMWxjcmUyMzV2dG9sb2poNGc0MDNlcC5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbQ=="
|
||||
);
|
||||
const CLIENT_SECRET = decode("R09DU1BYLUs1OEZXUjQ4NkxkTEoxbUxCOHNYQzR6NnFEQWY=");
|
||||
```
|
||||
|
||||
### Modes de flux OAuth
|
||||
|
||||
1. **Flux automatique** (machines locales avec navigateur) :
|
||||
- Ouvre le navigateur automatiquement
|
||||
- Le serveur de callback local capture la redirection
|
||||
- Aucune interaction utilisateur requise après l'authentification initiale
|
||||
|
||||
2. **Flux manuel** (distant/sans interface/WSL2) :
|
||||
- URL affichée pour copier-coller manuellement
|
||||
- L'utilisateur complète l'authentification dans un navigateur externe
|
||||
- L'utilisateur colle l'URL de redirection complète
|
||||
|
||||
```typescript
|
||||
function shouldUseManualOAuthFlow(isRemote: boolean): boolean {
|
||||
return isRemote || isWSL2Sync();
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Gestion des jetons
|
||||
|
||||
### Structure du profil d'authentification
|
||||
|
||||
```typescript
|
||||
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
|
||||
};
|
||||
```
|
||||
|
||||
### Rafraîchissement des jetons
|
||||
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
## Récupération de la liste des modèles
|
||||
|
||||
### Récupérer les modèles disponibles
|
||||
|
||||
```typescript
|
||||
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,
|
||||
},
|
||||
}));
|
||||
}
|
||||
```
|
||||
|
||||
### Format de réponse
|
||||
|
||||
```typescript
|
||||
type FetchAvailableModelsResponse = {
|
||||
models?: Record<string, {
|
||||
displayName?: string;
|
||||
quotaInfo?: {
|
||||
remainingFraction?: number | string;
|
||||
resetTime?: string; // Horodatage ISO 8601
|
||||
isExhausted?: boolean;
|
||||
};
|
||||
}>;
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Suivi de l'utilisation
|
||||
|
||||
### Récupérer les données d'utilisation
|
||||
|
||||
```typescript
|
||||
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,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### Structure de la réponse d'utilisation
|
||||
|
||||
```typescript
|
||||
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
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Structure du plugin fournisseur
|
||||
|
||||
### Définition du plugin
|
||||
|
||||
```typescript
|
||||
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
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
### ProviderAuthContext
|
||||
|
||||
```typescript
|
||||
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;
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
### ProviderAuthResult
|
||||
|
||||
```typescript
|
||||
type ProviderAuthResult = {
|
||||
profiles: Array<{
|
||||
profileId: string;
|
||||
credential: AuthProfileCredential;
|
||||
}>;
|
||||
configPatch?: Partial<PicoClawConfig>;
|
||||
defaultModel?: string;
|
||||
notes?: string[];
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Exigences d'intégration
|
||||
|
||||
### 1. Environnement/dépendances requis
|
||||
|
||||
- Go ≥ 1.25
|
||||
- Base de code PicoClaw (`pkg/providers/` et `pkg/auth/`)
|
||||
- Packages de la bibliothèque standard `crypto` et `net/http`
|
||||
|
||||
### 2. En-têtes requis pour les appels API
|
||||
|
||||
```typescript
|
||||
const 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",
|
||||
};
|
||||
```
|
||||
|
||||
### 3. Assainissement des schémas de modèles
|
||||
|
||||
Antigravity utilise des modèles compatibles Gemini, les schémas d'outils doivent donc être assainis :
|
||||
|
||||
```typescript
|
||||
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
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Gestion des blocs de réflexion (modèles Claude)
|
||||
|
||||
Pour les modèles Claude via Antigravity, les blocs de réflexion nécessitent un traitement spécial :
|
||||
|
||||
```typescript
|
||||
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
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Points de terminaison API
|
||||
|
||||
### Points de terminaison d'authentification
|
||||
|
||||
| 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) |
|
||||
|
||||
### Points de terminaison Cloud Code Assist
|
||||
|
||||
| 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 :
|
||||
|
||||
```json
|
||||
{
|
||||
"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` :
|
||||
|
||||
```json
|
||||
{
|
||||
"response": {
|
||||
"candidates": [...],
|
||||
"usageMetadata": {...},
|
||||
"modelVersion": "...",
|
||||
"responseId": "..."
|
||||
},
|
||||
"traceId": "...",
|
||||
"metadata": {}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
### Configuration config.json
|
||||
|
||||
```json
|
||||
{
|
||||
"model_list": [
|
||||
{
|
||||
"model_name": "gemini-flash",
|
||||
"model": "antigravity/gemini-3-flash",
|
||||
"auth_method": "oauth"
|
||||
}
|
||||
],
|
||||
"agents": {
|
||||
"defaults": {
|
||||
"model_name": "gemini-flash"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Stockage du profil d'authentification
|
||||
|
||||
Les profils d'authentification sont stockés dans `~/.picoclaw/auth.json` :
|
||||
|
||||
```json
|
||||
{
|
||||
"credentials": {
|
||||
"google-antigravity": {
|
||||
"access_token": "ya29...",
|
||||
"refresh_token": "1//...",
|
||||
"expires_at": "2026-01-01T00:00:00Z",
|
||||
"provider": "google-antigravity",
|
||||
"auth_method": "oauth",
|
||||
"email": "user@example.com",
|
||||
"project_id": "my-project-id"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Créer un nouveau fournisseur dans PicoClaw
|
||||
|
||||
Les fournisseurs PicoClaw sont implémentés en tant que packages Go sous `pkg/providers/`. Pour ajouter un nouveau fournisseur :
|
||||
|
||||
### Implémentation étape par étape
|
||||
|
||||
#### 1. Créer le fichier du fournisseur
|
||||
|
||||
Créez un nouveau fichier Go dans `pkg/providers/` :
|
||||
|
||||
```
|
||||
pkg/providers/
|
||||
└── your_provider.go
|
||||
```
|
||||
|
||||
#### 2. Implémenter l'interface Provider
|
||||
|
||||
Votre fournisseur doit implémenter l'interface `Provider` définie dans `pkg/providers/types.go` :
|
||||
|
||||
```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
|
||||
}
|
||||
```
|
||||
|
||||
#### 3. Enregistrer dans la factory
|
||||
|
||||
Ajoutez votre fournisseur au switch de protocole dans `pkg/providers/factory.go` :
|
||||
|
||||
```go
|
||||
case "your-provider":
|
||||
return NewYourProvider(sel.apiKey, sel.apiBase, sel.proxy), nil
|
||||
```
|
||||
|
||||
#### 4. Ajouter la configuration par défaut (optionnel)
|
||||
|
||||
Ajoutez une entrée par défaut dans `pkg/config/defaults.go` :
|
||||
|
||||
```go
|
||||
{
|
||||
ModelName: "your-model",
|
||||
Model: "your-provider/model-name",
|
||||
APIKey: "",
|
||||
},
|
||||
```
|
||||
|
||||
#### 5. Ajouter le support d'authentification (optionnel)
|
||||
|
||||
Si votre fournisseur nécessite OAuth ou une authentification spéciale, ajoutez un cas dans `cmd/picoclaw/internal/auth/helpers.go` :
|
||||
|
||||
```go
|
||||
case "your-provider":
|
||||
authLoginYourProvider()
|
||||
```
|
||||
|
||||
#### 6. Configurer via `config.json`
|
||||
|
||||
```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"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Tester votre implémentation
|
||||
|
||||
### Commandes CLI
|
||||
|
||||
```bash
|
||||
# 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
|
||||
```
|
||||
|
||||
### Variables d'environnement pour les tests
|
||||
|
||||
```bash
|
||||
# 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":"..."}]'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Références
|
||||
|
||||
- **Fichiers source :**
|
||||
- `pkg/providers/antigravity_provider.go` - Implémentation du fournisseur Antigravity
|
||||
- `pkg/auth/oauth.go` - Implémentation du flux OAuth
|
||||
- `pkg/auth/store.go` - Stockage des identifiants d'authentification (`~/.picoclaw/auth.json`)
|
||||
- `pkg/providers/factory.go` - Factory des fournisseurs et routage de protocole
|
||||
- `pkg/providers/types.go` - Définitions de l'interface fournisseur
|
||||
- `cmd/picoclaw/internal/auth/helpers.go` - Commandes CLI d'authentification
|
||||
|
||||
- **Documentation :**
|
||||
- `docs/ANTIGRAVITY_USAGE.md` - Guide d'utilisation d'Antigravity
|
||||
- `docs/migration/model-list-migration.md` - Guide de migration
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
1. **Projet Google Cloud :** Antigravity nécessite que Gemini for Google Cloud soit activé sur votre projet Google Cloud
|
||||
2. **Quotas :** Utilise les quotas du projet Google Cloud (pas de facturation séparée)
|
||||
3. **Accès aux modèles :** Les modèles disponibles dépendent de la configuration de votre projet Google Cloud
|
||||
4. **Blocs de réflexion :** Les modèles Claude via Antigravity nécessitent un traitement spécial des blocs de réflexion avec signatures
|
||||
5. **Assainissement des schémas :** Les schémas d'outils doivent être assainis pour supprimer les mots-clés JSON Schema non supportés
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
## Gestion des erreurs courantes
|
||||
|
||||
### 1. Limitation de débit (HTTP 429)
|
||||
|
||||
Antigravity 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 :**
|
||||
```json
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Réponses vides (modèles restreints)
|
||||
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
## Dépannage
|
||||
|
||||
### "Token expired" (jeton expiré)
|
||||
- Rafraîchir les jetons OAuth : `picoclaw auth login --provider antigravity`
|
||||
|
||||
### "Gemini for Google Cloud is not enabled" (Gemini for Google Cloud n'est pas activé)
|
||||
- Activer l'API dans votre Google Cloud Console
|
||||
|
||||
### "Project not found" (projet non trouvé)
|
||||
- Vérifier que votre projet Google Cloud a les API nécessaires activées
|
||||
- Vérifier que l'ID du projet est correctement récupéré lors de l'authentification
|
||||
|
||||
### Les modèles n'apparaissent pas dans la liste
|
||||
- Vérifier que l'authentification OAuth s'est terminée avec succès
|
||||
- Vérifier le stockage du profil d'authentification : `~/.picoclaw/auth.json`
|
||||
- Relancer `picoclaw auth login --provider antigravity`
|
||||
@@ -0,0 +1,809 @@
|
||||
> [README](../project/README.ja.md) に戻る
|
||||
|
||||
# Antigravity 認証・統合ガイド
|
||||
|
||||
## 概要
|
||||
|
||||
**Antigravity**(Google Cloud Code Assist)は、Google が提供する AI モデルプロバイダーで、Google のクラウドインフラストラクチャを通じて Claude Opus 4.6 や Gemini などのモデルへのアクセスを提供します。本ドキュメントでは、認証の仕組み、モデルの取得方法、PicoClaw での新しいプロバイダーの実装方法について完全なガイドを提供します。
|
||||
|
||||
---
|
||||
|
||||
## 目次
|
||||
|
||||
1. [認証フロー](#認証フロー)
|
||||
2. [OAuth 実装の詳細](#oauth-実装の詳細)
|
||||
3. [トークン管理](#トークン管理)
|
||||
4. [モデルリストの取得](#モデルリストの取得)
|
||||
5. [使用量トラッキング](#使用量トラッキング)
|
||||
6. [プロバイダープラグイン構造](#プロバイダープラグイン構造)
|
||||
7. [統合要件](#統合要件)
|
||||
8. [API エンドポイント](#api-エンドポイント)
|
||||
9. [設定](#設定)
|
||||
10. [PicoClaw での新しいプロバイダーの作成](#picoclaw-での新しいプロバイダーの作成)
|
||||
|
||||
---
|
||||
|
||||
## 認証フロー
|
||||
|
||||
### 1. PKCE 付き OAuth 2.0
|
||||
|
||||
Antigravity はセキュアな認証のために **OAuth 2.0 with PKCE(Proof Key for Code Exchange)** を使用します:
|
||||
|
||||
```
|
||||
┌─────────────┐ ┌─────────────────┐
|
||||
│ 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──── │ │
|
||||
└─────────────┘ └─────────────────┘
|
||||
```
|
||||
|
||||
### 2. 詳細手順
|
||||
|
||||
#### ステップ 1:PKCE パラメータの生成
|
||||
```typescript
|
||||
function generatePkce(): { verifier: string; challenge: string } {
|
||||
const verifier = randomBytes(32).toString("hex");
|
||||
const challenge = createHash("sha256").update(verifier).digest("base64url");
|
||||
return { verifier, challenge };
|
||||
}
|
||||
```
|
||||
|
||||
#### ステップ 2:認可 URL の構築
|
||||
```typescript
|
||||
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();
|
||||
}
|
||||
```
|
||||
|
||||
**必要なスコープ:**
|
||||
```typescript
|
||||
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",
|
||||
];
|
||||
```
|
||||
|
||||
#### ステップ 3:OAuth コールバックの処理
|
||||
|
||||
**自動モード(ローカル開発):**
|
||||
- ポート 51121 でローカル HTTP サーバーを起動
|
||||
- Google からのリダイレクトを待機
|
||||
- クエリパラメータから認可コードを抽出
|
||||
|
||||
**手動モード(リモート/ヘッドレス):**
|
||||
- ユーザーに認可 URL を表示
|
||||
- ユーザーがブラウザで認証を完了
|
||||
- ユーザーが完全なリダイレクト URL をターミナルに貼り付け
|
||||
- 貼り付けられた URL からコードを解析
|
||||
|
||||
#### ステップ 4:コードをトークンに交換
|
||||
```typescript
|
||||
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
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
#### ステップ 5:追加のユーザーデータの取得
|
||||
|
||||
**ユーザーメール:**
|
||||
```typescript
|
||||
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(API 呼び出しに必須):**
|
||||
```typescript
|
||||
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"; // デフォルトのフォールバック
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## OAuth 実装の詳細
|
||||
|
||||
### クライアント認証情報
|
||||
|
||||
**重要:** これらは pi-ai との同期のためにソースコード内で base64 エンコードされています:
|
||||
|
||||
```typescript
|
||||
const decode = (s: string) => Buffer.from(s, "base64").toString();
|
||||
|
||||
const CLIENT_ID = decode(
|
||||
"MTA3MTAwNjA2MDU5MS10bWhzc2luMmgyMWxjcmUyMzV2dG9sb2poNGc0MDNlcC5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbQ=="
|
||||
);
|
||||
const CLIENT_SECRET = decode("R09DU1BYLUs1OEZXUjQ4NkxkTEoxbUxCOHNYQzR6NnFEQWY=");
|
||||
```
|
||||
|
||||
### OAuth フローモード
|
||||
|
||||
1. **自動フロー**(ブラウザのあるローカルマシン):
|
||||
- ブラウザを自動的に開く
|
||||
- ローカルコールバックサーバーがリダイレクトをキャプチャ
|
||||
- 初回認証後はユーザー操作不要
|
||||
|
||||
2. **手動フロー**(リモート/ヘッドレス/WSL2):
|
||||
- 手動コピー&ペースト用の URL を表示
|
||||
- ユーザーが外部ブラウザで認証を完了
|
||||
- ユーザーが完全なリダイレクト URL を貼り付け
|
||||
|
||||
```typescript
|
||||
function shouldUseManualOAuthFlow(isRemote: boolean): boolean {
|
||||
return isRemote || isWSL2Sync();
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## トークン管理
|
||||
|
||||
### 認証プロファイル構造
|
||||
|
||||
```typescript
|
||||
type OAuthCredential = {
|
||||
type: "oauth";
|
||||
provider: "google-antigravity";
|
||||
access: string; // アクセストークン
|
||||
refresh: string; // リフレッシュトークン
|
||||
expires: number; // 有効期限タイムスタンプ(エポックからのミリ秒)
|
||||
email?: string; // ユーザーメール
|
||||
projectId?: string; // Google Cloud プロジェクト ID
|
||||
};
|
||||
```
|
||||
|
||||
### トークンの更新
|
||||
|
||||
認証情報にはリフレッシュトークンが含まれており、現在のアクセストークンが期限切れになった際に新しいアクセストークンを取得するために使用できます。有効期限は競合状態を防ぐために 5 分のバッファを設けています。
|
||||
|
||||
---
|
||||
|
||||
## モデルリストの取得
|
||||
|
||||
### 利用可能なモデルの取得
|
||||
|
||||
```typescript
|
||||
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();
|
||||
|
||||
// クォータ情報付きのモデルを返す
|
||||
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,
|
||||
},
|
||||
}));
|
||||
}
|
||||
```
|
||||
|
||||
### レスポンス形式
|
||||
|
||||
```typescript
|
||||
type FetchAvailableModelsResponse = {
|
||||
models?: Record<string, {
|
||||
displayName?: string;
|
||||
quotaInfo?: {
|
||||
remainingFraction?: number | string;
|
||||
resetTime?: string; // ISO 8601 タイムスタンプ
|
||||
isExhausted?: boolean;
|
||||
};
|
||||
}>;
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 使用量トラッキング
|
||||
|
||||
### 使用量データの取得
|
||||
|
||||
```typescript
|
||||
export async function fetchAntigravityUsage(
|
||||
token: string,
|
||||
timeoutMs: number
|
||||
): Promise<ProviderUsageSnapshot> {
|
||||
// 1. クレジットとプラン情報を取得
|
||||
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",
|
||||
},
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
// クレジット情報を抽出
|
||||
const { availablePromptCredits, planInfo, currentTier } = data;
|
||||
|
||||
// 2. モデルクォータを取得
|
||||
const modelsRes = await fetch(
|
||||
`${BASE_URL}/v1internal:fetchAvailableModels`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
body: JSON.stringify({ project: projectId }),
|
||||
}
|
||||
);
|
||||
|
||||
// 使用量ウィンドウを構築
|
||||
return {
|
||||
provider: "google-antigravity",
|
||||
displayName: "Google Antigravity",
|
||||
windows: [
|
||||
{ label: "Credits", usedPercent: calculateUsedPercent(available, monthly) },
|
||||
// 個別モデルクォータ...
|
||||
],
|
||||
plan: currentTier?.name || planType,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### 使用量レスポンス構造
|
||||
|
||||
```typescript
|
||||
type ProviderUsageSnapshot = {
|
||||
provider: "google-antigravity";
|
||||
displayName: string;
|
||||
windows: UsageWindow[];
|
||||
plan?: string;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
type UsageWindow = {
|
||||
label: string; // "Credits" またはモデル ID
|
||||
usedPercent: number; // 0-100
|
||||
resetAt?: number; // クォータがリセットされるタイムスタンプ
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## プロバイダープラグイン構造
|
||||
|
||||
### プラグイン定義
|
||||
|
||||
```typescript
|
||||
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) => {
|
||||
// OAuth 実装はここに記述
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
### ProviderAuthContext
|
||||
|
||||
```typescript
|
||||
type ProviderAuthContext = {
|
||||
config: PicoClawConfig;
|
||||
agentDir?: string;
|
||||
workspaceDir?: string;
|
||||
prompter: WizardPrompter; // UI プロンプト/通知
|
||||
runtime: RuntimeEnv; // ログなど
|
||||
isRemote: boolean; // リモート実行かどうか
|
||||
openUrl: (url: string) => Promise<void>; // ブラウザオープナー
|
||||
oauth: {
|
||||
createVpsAwareHandlers: Function;
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
### ProviderAuthResult
|
||||
|
||||
```typescript
|
||||
type ProviderAuthResult = {
|
||||
profiles: Array<{
|
||||
profileId: string;
|
||||
credential: AuthProfileCredential;
|
||||
}>;
|
||||
configPatch?: Partial<PicoClawConfig>;
|
||||
defaultModel?: string;
|
||||
notes?: string[];
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 統合要件
|
||||
|
||||
### 1. 必要な環境/依存関係
|
||||
|
||||
- Go ≥ 1.25
|
||||
- PicoClaw コードベース(`pkg/providers/` および `pkg/auth/`)
|
||||
- `crypto` および `net/http` 標準ライブラリパッケージ
|
||||
|
||||
### 2. API 呼び出しに必要なヘッダー
|
||||
|
||||
```typescript
|
||||
const REQUIRED_HEADERS = {
|
||||
"Authorization": `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": "antigravity", // または "google-api-nodejs-client/9.15.1"
|
||||
"X-Goog-Api-Client": "google-cloud-sdk vscode_cloudshelleditor/0.1",
|
||||
};
|
||||
|
||||
// loadCodeAssist 呼び出しには以下も含める:
|
||||
const CLIENT_METADATA = {
|
||||
ideType: "ANTIGRAVITY", // または "IDE_UNSPECIFIED"
|
||||
platform: "PLATFORM_UNSPECIFIED",
|
||||
pluginType: "GEMINI",
|
||||
};
|
||||
```
|
||||
|
||||
### 3. モデルスキーマのサニタイズ
|
||||
|
||||
Antigravity は Gemini 互換モデルを使用するため、ツールスキーマのサニタイズが必要です:
|
||||
|
||||
```typescript
|
||||
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",
|
||||
]);
|
||||
|
||||
// 送信前にスキーマをクリーンアップ
|
||||
function cleanToolSchemaForGemini(schema: Record<string, unknown>): unknown {
|
||||
// サポートされていないキーワードを削除
|
||||
// トップレベルに type: "object" があることを確認
|
||||
// anyOf/oneOf ユニオンをフラット化
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 思考ブロックの処理(Claude モデル)
|
||||
|
||||
Antigravity の Claude モデルでは、思考ブロックに特別な処理が必要です:
|
||||
|
||||
```typescript
|
||||
const ANTIGRAVITY_SIGNATURE_RE = /^[A-Za-z0-9+/]+={0,2}$/;
|
||||
|
||||
export function sanitizeAntigravityThinkingBlocks(
|
||||
messages: AgentMessage[]
|
||||
): AgentMessage[] {
|
||||
// 思考シグネチャを検証
|
||||
// シグネチャフィールドを正規化
|
||||
// 署名されていない思考ブロックを破棄
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API エンドポイント
|
||||
|
||||
### 認証エンドポイント
|
||||
|
||||
| エンドポイント | メソッド | 用途 |
|
||||
|---------------|---------|------|
|
||||
| `https://accounts.google.com/o/oauth2/v2/auth` | GET | OAuth 認可 |
|
||||
| `https://oauth2.googleapis.com/token` | POST | トークン交換 |
|
||||
| `https://www.googleapis.com/oauth2/v1/userinfo` | GET | ユーザー情報(メール) |
|
||||
|
||||
### Cloud Code Assist エンドポイント
|
||||
|
||||
| エンドポイント | メソッド | 用途 |
|
||||
|---------------|---------|------|
|
||||
| `https://cloudcode-pa.googleapis.com/v1internal:loadCodeAssist` | POST | プロジェクト情報、クレジット、プランの読み込み |
|
||||
| `https://cloudcode-pa.googleapis.com/v1internal:fetchAvailableModels` | POST | クォータ付き利用可能モデルの一覧 |
|
||||
| `https://cloudcode-pa.googleapis.com/v1internal:streamGenerateContent?alt=sse` | POST | チャットストリーミングエンドポイント |
|
||||
|
||||
**API リクエスト形式(チャット):**
|
||||
`v1internal:streamGenerateContent` エンドポイントは、標準の Gemini リクエストをラップするエンベロープ形式を期待します:
|
||||
|
||||
```json
|
||||
{
|
||||
"project": "your-project-id",
|
||||
"model": "model-id",
|
||||
"request": {
|
||||
"contents": [...],
|
||||
"systemInstruction": {...},
|
||||
"generationConfig": {...},
|
||||
"tools": [...]
|
||||
},
|
||||
"requestType": "agent",
|
||||
"userAgent": "antigravity",
|
||||
"requestId": "agent-timestamp-random"
|
||||
}
|
||||
```
|
||||
|
||||
**API レスポンス形式(SSE):**
|
||||
各 SSE メッセージ(`data: {...}`)は `response` フィールドでラップされます:
|
||||
|
||||
```json
|
||||
{
|
||||
"response": {
|
||||
"candidates": [...],
|
||||
"usageMetadata": {...},
|
||||
"modelVersion": "...",
|
||||
"responseId": "..."
|
||||
},
|
||||
"traceId": "...",
|
||||
"metadata": {}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 設定
|
||||
|
||||
### config.json の設定
|
||||
|
||||
```json
|
||||
{
|
||||
"model_list": [
|
||||
{
|
||||
"model_name": "gemini-flash",
|
||||
"model": "antigravity/gemini-3-flash",
|
||||
"auth_method": "oauth"
|
||||
}
|
||||
],
|
||||
"agents": {
|
||||
"defaults": {
|
||||
"model_name": "gemini-flash"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 認証プロファイルの保存
|
||||
|
||||
認証プロファイルは `~/.picoclaw/auth.json` に保存されます:
|
||||
|
||||
```json
|
||||
{
|
||||
"credentials": {
|
||||
"google-antigravity": {
|
||||
"access_token": "ya29...",
|
||||
"refresh_token": "1//...",
|
||||
"expires_at": "2026-01-01T00:00:00Z",
|
||||
"provider": "google-antigravity",
|
||||
"auth_method": "oauth",
|
||||
"email": "user@example.com",
|
||||
"project_id": "my-project-id"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## PicoClaw での新しいプロバイダーの作成
|
||||
|
||||
PicoClaw のプロバイダーは `pkg/providers/` 配下の Go パッケージとして実装されます。新しいプロバイダーを追加するには:
|
||||
|
||||
### ステップバイステップの実装
|
||||
|
||||
#### 1. プロバイダーファイルの作成
|
||||
|
||||
`pkg/providers/` に新しい Go ファイルを作成します:
|
||||
|
||||
```
|
||||
pkg/providers/
|
||||
└── your_provider.go
|
||||
```
|
||||
|
||||
#### 2. Provider インターフェースの実装
|
||||
|
||||
プロバイダーは `pkg/providers/types.go` で定義された `Provider` インターフェースを実装する必要があります:
|
||||
|
||||
```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 {
|
||||
// ストリーミング付きチャット補完を実装
|
||||
}
|
||||
```
|
||||
|
||||
#### 3. ファクトリーへの登録
|
||||
|
||||
`pkg/providers/factory.go` のプロトコルスイッチにプロバイダーを追加します:
|
||||
|
||||
```go
|
||||
case "your-provider":
|
||||
return NewYourProvider(sel.apiKey, sel.apiBase, sel.proxy), nil
|
||||
```
|
||||
|
||||
#### 4. デフォルト設定の追加(オプション)
|
||||
|
||||
`pkg/config/defaults.go` にデフォルトエントリを追加します:
|
||||
|
||||
```go
|
||||
{
|
||||
ModelName: "your-model",
|
||||
Model: "your-provider/model-name",
|
||||
APIKey: "",
|
||||
},
|
||||
```
|
||||
|
||||
#### 5. 認証サポートの追加(オプション)
|
||||
|
||||
プロバイダーが OAuth や特別な認証を必要とする場合、`cmd/picoclaw/internal/auth/helpers.go` にケースを追加します:
|
||||
|
||||
```go
|
||||
case "your-provider":
|
||||
authLoginYourProvider()
|
||||
```
|
||||
|
||||
#### 6. `config.json` での設定
|
||||
|
||||
```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"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 実装のテスト
|
||||
|
||||
### CLI コマンド
|
||||
|
||||
```bash
|
||||
# プロバイダーで認証
|
||||
picoclaw auth login --provider your-provider
|
||||
|
||||
# モデルの一覧表示(Antigravity 用)
|
||||
picoclaw auth models
|
||||
|
||||
# ゲートウェイの起動
|
||||
picoclaw gateway
|
||||
|
||||
# 特定のモデルでエージェントを実行
|
||||
picoclaw agent -m "Hello" --model your-model
|
||||
```
|
||||
|
||||
### テスト用環境変数
|
||||
|
||||
```bash
|
||||
# デフォルトモデルの上書き
|
||||
export PICOCLAW_AGENTS_DEFAULTS_MODEL=your-model
|
||||
|
||||
# プロバイダー設定の上書き
|
||||
export PICOCLAW_MODEL_LIST='[{"model_name":"your-model","model":"your-provider/model-name","api_key":"..."}]'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 参考資料
|
||||
|
||||
- **ソースファイル:**
|
||||
- `pkg/providers/antigravity_provider.go` - Antigravity プロバイダー実装
|
||||
- `pkg/auth/oauth.go` - OAuth フロー実装
|
||||
- `pkg/auth/store.go` - 認証情報ストレージ(`~/.picoclaw/auth.json`)
|
||||
- `pkg/providers/factory.go` - プロバイダーファクトリーとプロトコルルーティング
|
||||
- `pkg/providers/types.go` - プロバイダーインターフェース定義
|
||||
- `cmd/picoclaw/internal/auth/helpers.go` - 認証 CLI コマンド
|
||||
|
||||
- **ドキュメント:**
|
||||
- `docs/ANTIGRAVITY_USAGE.md` - Antigravity 使用ガイド
|
||||
- `docs/migration/model-list-migration.md` - 移行ガイド
|
||||
|
||||
---
|
||||
|
||||
## 注意事項
|
||||
|
||||
1. **Google Cloud プロジェクト:** Antigravity は Google Cloud プロジェクトで Gemini for Google Cloud が有効になっている必要があります
|
||||
2. **クォータ:** Google Cloud プロジェクトのクォータを使用します(個別の課金ではありません)
|
||||
3. **モデルアクセス:** 利用可能なモデルは Google Cloud プロジェクトの設定に依存します
|
||||
4. **思考ブロック:** Antigravity 経由の Claude モデルは、署名付き思考ブロックの特別な処理が必要です
|
||||
5. **スキーマサニタイズ:** ツールスキーマはサポートされていない JSON Schema キーワードを削除するためにサニタイズが必要です
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
## 一般的なエラー処理
|
||||
|
||||
### 1. レート制限(HTTP 429)
|
||||
|
||||
プロジェクト/モデルのクォータが枯渇すると、Antigravity は 429 エラーを返します。エラーレスポンスには通常、`details` フィールドに `quotaResetDelay` が含まれます。
|
||||
|
||||
**429 エラーの例:**
|
||||
```json
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 空のレスポンス(制限付きモデル)
|
||||
|
||||
一部のモデルは利用可能モデルリストに表示されますが、空のレスポンスを返す場合があります(200 OK だが SSE ストリームが空)。これは通常、現在のプロジェクトに使用権限がないプレビュー版または制限付きモデルで発生します。
|
||||
|
||||
**対処法:** 空のレスポンスをエラーとして扱い、そのモデルがプロジェクトに対して制限されているか無効である可能性があることをユーザーに通知します。
|
||||
|
||||
---
|
||||
|
||||
## トラブルシューティング
|
||||
|
||||
### "Token expired"(トークン期限切れ)
|
||||
- OAuth トークンを更新:`picoclaw auth login --provider antigravity`
|
||||
|
||||
### "Gemini for Google Cloud is not enabled"(Gemini for Google Cloud が有効になっていない)
|
||||
- Google Cloud Console で API を有効にしてください
|
||||
|
||||
### "Project not found"(プロジェクトが見つからない)
|
||||
- Google Cloud プロジェクトで必要な API が有効になっていることを確認してください
|
||||
- 認証中にプロジェクト ID が正しく取得されているか確認してください
|
||||
|
||||
### モデルがリストに表示されない
|
||||
- OAuth 認証が正常に完了したことを確認してください
|
||||
- 認証プロファイルストレージを確認:`~/.picoclaw/auth.json`
|
||||
- `picoclaw auth login --provider antigravity` を再実行してください
|
||||
@@ -0,0 +1,807 @@
|
||||
# Antigravity Authentication & Integration Guide
|
||||
|
||||
## Overview
|
||||
|
||||
**Antigravity** (Google Cloud Code Assist) is a Google-backed AI model provider that offers access to models like Claude Opus 4.6 and Gemini through Google's Cloud infrastructure. This document provides a complete guide on how authentication works, how to fetch models, and how to implement a new provider in PicoClaw.
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Authentication Flow](#authentication-flow)
|
||||
2. [OAuth Implementation Details](#oauth-implementation-details)
|
||||
3. [Token Management](#token-management)
|
||||
4. [Models List Fetching](#models-list-fetching)
|
||||
5. [Usage Tracking](#usage-tracking)
|
||||
6. [Provider Plugin Structure](#provider-plugin-structure)
|
||||
7. [Integration Requirements](#integration-requirements)
|
||||
8. [API Endpoints](#api-endpoints)
|
||||
9. [Configuration](#configuration)
|
||||
10. [Creating a New Provider in PicoClaw](#creating-a-new-provider-in-picoclaw)
|
||||
|
||||
---
|
||||
|
||||
## Authentication Flow
|
||||
|
||||
### 1. OAuth 2.0 with PKCE
|
||||
|
||||
Antigravity uses **OAuth 2.0 with PKCE (Proof Key for Code Exchange)** for secure authentication:
|
||||
|
||||
```
|
||||
┌─────────────┐ ┌─────────────────┐
|
||||
│ 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──── │ │
|
||||
└─────────────┘ └─────────────────┘
|
||||
```
|
||||
|
||||
### 2. Detailed Steps
|
||||
|
||||
#### Step 1: Generate PKCE Parameters
|
||||
```typescript
|
||||
function generatePkce(): { verifier: string; challenge: string } {
|
||||
const verifier = randomBytes(32).toString("hex");
|
||||
const challenge = createHash("sha256").update(verifier).digest("base64url");
|
||||
return { verifier, challenge };
|
||||
}
|
||||
```
|
||||
|
||||
#### Step 2: Build Authorization URL
|
||||
```typescript
|
||||
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();
|
||||
}
|
||||
```
|
||||
|
||||
**Required Scopes:**
|
||||
```typescript
|
||||
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",
|
||||
];
|
||||
```
|
||||
|
||||
#### Step 3: Handle OAuth Callback
|
||||
|
||||
**Automatic Mode (Local Development):**
|
||||
- Start a local HTTP server on port 51121
|
||||
- Wait for the redirect from Google
|
||||
- Extract the authorization code from the query parameters
|
||||
|
||||
**Manual Mode (Remote/Headless):**
|
||||
- Display the authorization URL to the user
|
||||
- User completes authentication in their browser
|
||||
- User pastes the full redirect URL back into the terminal
|
||||
- Parse the code from the pasted URL
|
||||
|
||||
#### Step 4: Exchange Code for Tokens
|
||||
```typescript
|
||||
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
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
#### Step 5: Fetch Additional User Data
|
||||
|
||||
**User Email:**
|
||||
```typescript
|
||||
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;
|
||||
}
|
||||
```
|
||||
|
||||
**Project ID (Required for API calls):**
|
||||
```typescript
|
||||
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"; // Default fallback
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## OAuth Implementation Details
|
||||
|
||||
### Client Credentials
|
||||
|
||||
**Important:** These are base64-encoded in the source code for sync with pi-ai:
|
||||
|
||||
```typescript
|
||||
const decode = (s: string) => Buffer.from(s, "base64").toString();
|
||||
|
||||
const CLIENT_ID = decode(
|
||||
"MTA3MTAwNjA2MDU5MS10bWhzc2luMmgyMWxjcmUyMzV2dG9sb2poNGc0MDNlcC5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbQ=="
|
||||
);
|
||||
const CLIENT_SECRET = decode("R09DU1BYLUs1OEZXUjQ4NkxkTEoxbUxCOHNYQzR6NnFEQWY=");
|
||||
```
|
||||
|
||||
### OAuth Flow Modes
|
||||
|
||||
1. **Automatic Flow** (Local machines with browser):
|
||||
- Opens browser automatically
|
||||
- Local callback server captures redirect
|
||||
- No user interaction required after initial auth
|
||||
|
||||
2. **Manual Flow** (Remote/headless/WSL2):
|
||||
- URL displayed for manual copy-paste
|
||||
- User completes auth in external browser
|
||||
- User pastes full redirect URL back
|
||||
|
||||
```typescript
|
||||
function shouldUseManualOAuthFlow(isRemote: boolean): boolean {
|
||||
return isRemote || isWSL2Sync();
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Token Management
|
||||
|
||||
### Auth Profile Structure
|
||||
|
||||
```typescript
|
||||
type OAuthCredential = {
|
||||
type: "oauth";
|
||||
provider: "google-antigravity";
|
||||
access: string; // Access token
|
||||
refresh: string; // Refresh token
|
||||
expires: number; // Expiration timestamp (ms since epoch)
|
||||
email?: string; // User email
|
||||
projectId?: string; // Google Cloud project ID
|
||||
};
|
||||
```
|
||||
|
||||
### Token Refresh
|
||||
|
||||
The credential includes a refresh token that can be used to obtain new access tokens when the current one expires. The expiration is set with a 5-minute buffer to prevent race conditions.
|
||||
|
||||
---
|
||||
|
||||
## Models List Fetching
|
||||
|
||||
### Fetch Available Models
|
||||
|
||||
```typescript
|
||||
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();
|
||||
|
||||
// Returns models with quota information
|
||||
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,
|
||||
},
|
||||
}));
|
||||
}
|
||||
```
|
||||
|
||||
### Response Format
|
||||
|
||||
```typescript
|
||||
type FetchAvailableModelsResponse = {
|
||||
models?: Record<string, {
|
||||
displayName?: string;
|
||||
quotaInfo?: {
|
||||
remainingFraction?: number | string;
|
||||
resetTime?: string; // ISO 8601 timestamp
|
||||
isExhausted?: boolean;
|
||||
};
|
||||
}>;
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Usage Tracking
|
||||
|
||||
### Fetch Usage Data
|
||||
|
||||
```typescript
|
||||
export async function fetchAntigravityUsage(
|
||||
token: string,
|
||||
timeoutMs: number
|
||||
): Promise<ProviderUsageSnapshot> {
|
||||
// 1. Fetch credits and plan info
|
||||
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",
|
||||
},
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
// Extract credits info
|
||||
const { availablePromptCredits, planInfo, currentTier } = data;
|
||||
|
||||
// 2. Fetch model quotas
|
||||
const modelsRes = await fetch(
|
||||
`${BASE_URL}/v1internal:fetchAvailableModels`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
body: JSON.stringify({ project: projectId }),
|
||||
}
|
||||
);
|
||||
|
||||
// Build usage windows
|
||||
return {
|
||||
provider: "google-antigravity",
|
||||
displayName: "Google Antigravity",
|
||||
windows: [
|
||||
{ label: "Credits", usedPercent: calculateUsedPercent(available, monthly) },
|
||||
// Individual model quotas...
|
||||
],
|
||||
plan: currentTier?.name || planType,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### Usage Response Structure
|
||||
|
||||
```typescript
|
||||
type ProviderUsageSnapshot = {
|
||||
provider: "google-antigravity";
|
||||
displayName: string;
|
||||
windows: UsageWindow[];
|
||||
plan?: string;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
type UsageWindow = {
|
||||
label: string; // "Credits" or model ID
|
||||
usedPercent: number; // 0-100
|
||||
resetAt?: number; // Timestamp when quota resets
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Provider Plugin Structure
|
||||
|
||||
### Plugin Definition
|
||||
|
||||
```typescript
|
||||
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) => {
|
||||
// OAuth implementation here
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
### ProviderAuthContext
|
||||
|
||||
```typescript
|
||||
type ProviderAuthContext = {
|
||||
config: PicoClawConfig;
|
||||
agentDir?: string;
|
||||
workspaceDir?: string;
|
||||
prompter: WizardPrompter; // UI prompts/notifications
|
||||
runtime: RuntimeEnv; // Logging, etc.
|
||||
isRemote: boolean; // Whether running remotely
|
||||
openUrl: (url: string) => Promise<void>; // Browser opener
|
||||
oauth: {
|
||||
createVpsAwareHandlers: Function;
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
### ProviderAuthResult
|
||||
|
||||
```typescript
|
||||
type ProviderAuthResult = {
|
||||
profiles: Array<{
|
||||
profileId: string;
|
||||
credential: AuthProfileCredential;
|
||||
}>;
|
||||
configPatch?: Partial<PicoClawConfig>;
|
||||
defaultModel?: string;
|
||||
notes?: string[];
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Integration Requirements
|
||||
|
||||
### 1. Required Environment/Dependencies
|
||||
|
||||
- Go ≥ 1.25
|
||||
- PicoClaw codebase (`pkg/providers/` and `pkg/auth/`)
|
||||
- `crypto` and `net/http` standard library packages
|
||||
|
||||
### 2. Required Headers for API Calls
|
||||
|
||||
```typescript
|
||||
const REQUIRED_HEADERS = {
|
||||
"Authorization": `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": "antigravity", // or "google-api-nodejs-client/9.15.1"
|
||||
"X-Goog-Api-Client": "google-cloud-sdk vscode_cloudshelleditor/0.1",
|
||||
};
|
||||
|
||||
// For loadCodeAssist calls, also include:
|
||||
const CLIENT_METADATA = {
|
||||
ideType: "ANTIGRAVITY", // or "IDE_UNSPECIFIED"
|
||||
platform: "PLATFORM_UNSPECIFIED",
|
||||
pluginType: "GEMINI",
|
||||
};
|
||||
```
|
||||
|
||||
### 3. Model Schema Sanitization
|
||||
|
||||
Antigravity uses Gemini-compatible models, so tool schemas must be sanitized:
|
||||
|
||||
```typescript
|
||||
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",
|
||||
]);
|
||||
|
||||
// Clean schema before sending
|
||||
function cleanToolSchemaForGemini(schema: Record<string, unknown>): unknown {
|
||||
// Remove unsupported keywords
|
||||
// Ensure top-level has type: "object"
|
||||
// Flatten anyOf/oneOf unions
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Thinking Block Handling (Claude Models)
|
||||
|
||||
For Antigravity Claude models, thinking blocks require special handling:
|
||||
|
||||
```typescript
|
||||
const ANTIGRAVITY_SIGNATURE_RE = /^[A-Za-z0-9+/]+={0,2}$/;
|
||||
|
||||
export function sanitizeAntigravityThinkingBlocks(
|
||||
messages: AgentMessage[]
|
||||
): AgentMessage[] {
|
||||
// Validate thinking signatures
|
||||
// Normalize signature fields
|
||||
// Discard unsigned thinking blocks
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### Authentication Endpoints
|
||||
|
||||
| Endpoint | Method | Purpose |
|
||||
|----------|--------|---------|
|
||||
| `https://accounts.google.com/o/oauth2/v2/auth` | GET | OAuth authorization |
|
||||
| `https://oauth2.googleapis.com/token` | POST | Token exchange |
|
||||
| `https://www.googleapis.com/oauth2/v1/userinfo` | GET | User info (email) |
|
||||
|
||||
### Cloud Code Assist Endpoints
|
||||
|
||||
| Endpoint | Method | Purpose |
|
||||
|----------|--------|---------|
|
||||
| `https://cloudcode-pa.googleapis.com/v1internal:loadCodeAssist` | POST | Load project info, credits, plan |
|
||||
| `https://cloudcode-pa.googleapis.com/v1internal:fetchAvailableModels` | POST | List available models with quotas |
|
||||
| `https://cloudcode-pa.googleapis.com/v1internal:streamGenerateContent?alt=sse` | POST | Chat streaming endpoint |
|
||||
|
||||
**API Request Format (Chat):**
|
||||
The `v1internal:streamGenerateContent` endpoint expects an envelope wrapping the standard Gemini request:
|
||||
|
||||
```json
|
||||
{
|
||||
"project": "your-project-id",
|
||||
"model": "model-id",
|
||||
"request": {
|
||||
"contents": [...],
|
||||
"systemInstruction": {...},
|
||||
"generationConfig": {...},
|
||||
"tools": [...]
|
||||
},
|
||||
"requestType": "agent",
|
||||
"userAgent": "antigravity",
|
||||
"requestId": "agent-timestamp-random"
|
||||
}
|
||||
```
|
||||
|
||||
**API Response Format (SSE):**
|
||||
Each SSE message (`data: {...}`) is wrapped in a `response` field:
|
||||
|
||||
```json
|
||||
{
|
||||
"response": {
|
||||
"candidates": [...],
|
||||
"usageMetadata": {...},
|
||||
"modelVersion": "...",
|
||||
"responseId": "..."
|
||||
},
|
||||
"traceId": "...",
|
||||
"metadata": {}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
### config.json Configuration
|
||||
|
||||
```json
|
||||
{
|
||||
"model_list": [
|
||||
{
|
||||
"model_name": "gemini-flash",
|
||||
"model": "antigravity/gemini-3-flash",
|
||||
"auth_method": "oauth"
|
||||
}
|
||||
],
|
||||
"agents": {
|
||||
"defaults": {
|
||||
"model_name": "gemini-flash"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Auth Profile Storage
|
||||
|
||||
Auth profiles are stored in `~/.picoclaw/auth.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"credentials": {
|
||||
"google-antigravity": {
|
||||
"access_token": "ya29...",
|
||||
"refresh_token": "1//...",
|
||||
"expires_at": "2026-01-01T00:00:00Z",
|
||||
"provider": "google-antigravity",
|
||||
"auth_method": "oauth",
|
||||
"email": "user@example.com",
|
||||
"project_id": "my-project-id"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Creating a New Provider in PicoClaw
|
||||
|
||||
PicoClaw providers are implemented as Go packages under `pkg/providers/`. To add a new provider:
|
||||
|
||||
### Step-by-Step Implementation
|
||||
|
||||
#### 1. Create Provider File
|
||||
|
||||
Create a new Go file in `pkg/providers/`:
|
||||
|
||||
```
|
||||
pkg/providers/
|
||||
└── your_provider.go
|
||||
```
|
||||
|
||||
#### 2. Implement the Provider Interface
|
||||
|
||||
Your provider must implement the `Provider` interface defined in `pkg/providers/types.go`:
|
||||
|
||||
```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 {
|
||||
// Implement chat completion with streaming
|
||||
}
|
||||
```
|
||||
|
||||
#### 3. Register in the Factory
|
||||
|
||||
Add your provider to the protocol switch in `pkg/providers/factory.go`:
|
||||
|
||||
```go
|
||||
case "your-provider":
|
||||
return NewYourProvider(sel.apiKey, sel.apiBase, sel.proxy), nil
|
||||
```
|
||||
|
||||
#### 4. Add Default Config (Optional)
|
||||
|
||||
Add a default entry in `pkg/config/defaults.go`:
|
||||
|
||||
```go
|
||||
{
|
||||
ModelName: "your-model",
|
||||
Model: "your-provider/model-name",
|
||||
APIKey: "",
|
||||
},
|
||||
```
|
||||
|
||||
#### 5. Add Auth Support (Optional)
|
||||
|
||||
If your provider requires OAuth or special authentication, add a case to `cmd/picoclaw/internal/auth/helpers.go`:
|
||||
|
||||
```go
|
||||
case "your-provider":
|
||||
authLoginYourProvider()
|
||||
```
|
||||
|
||||
#### 6. Configure via `config.json`
|
||||
|
||||
```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"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing Your Implementation
|
||||
|
||||
### CLI Commands
|
||||
|
||||
```bash
|
||||
# Authenticate with a provider
|
||||
picoclaw auth login --provider your-provider
|
||||
|
||||
# List models (for Antigravity)
|
||||
picoclaw auth models
|
||||
|
||||
# Start the gateway
|
||||
picoclaw gateway
|
||||
|
||||
# Run an agent with a specific model
|
||||
picoclaw agent -m "Hello" --model your-model
|
||||
```
|
||||
|
||||
### Environment Variables for Testing
|
||||
|
||||
```bash
|
||||
# Override default model
|
||||
export PICOCLAW_AGENTS_DEFAULTS_MODEL=your-model
|
||||
|
||||
# Override provider settings
|
||||
export PICOCLAW_MODEL_LIST='[{"model_name":"your-model","model":"your-provider/model-name","api_key":"..."}]'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- **Source Files:**
|
||||
- `pkg/providers/antigravity_provider.go` - Antigravity provider implementation
|
||||
- `pkg/auth/oauth.go` - OAuth flow implementation
|
||||
- `pkg/auth/store.go` - Auth credential storage (`~/.picoclaw/auth.json`)
|
||||
- `pkg/providers/factory.go` - Provider factory and protocol routing
|
||||
- `pkg/providers/types.go` - Provider interface definitions
|
||||
- `cmd/picoclaw/internal/auth/helpers.go` - Auth CLI commands
|
||||
|
||||
- **Documentation:**
|
||||
- `docs/ANTIGRAVITY_USAGE.md` - Antigravity usage guide
|
||||
- `docs/migration/model-list-migration.md` - Migration guide
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
1. **Google Cloud Project:** Antigravity requires Gemini for Google Cloud to be enabled on your Google Cloud project
|
||||
2. **Quotas:** Uses Google Cloud project quotas (not separate billing)
|
||||
3. **Model Access:** Available models depend on your Google Cloud project configuration
|
||||
4. **Thinking Blocks:** Claude models via Antigravity require special handling of thinking blocks with signatures
|
||||
5. **Schema Sanitization:** Tool schemas must be sanitized to remove unsupported JSON Schema keywords
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
## Common Error Handling
|
||||
|
||||
### 1. Rate Limiting (HTTP 429)
|
||||
|
||||
Antigravity returns a 429 error when project/model quotas are exhausted. The error response often contains a `quotaResetDelay` in the `details` field.
|
||||
|
||||
**Example 429 Error:**
|
||||
```json
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Empty Responses (Restricted Models)
|
||||
|
||||
Some models might show up in the available models list but return an empty response (200 OK but empty SSE stream). This usually happens for preview or restricted models that the current project doesn't have permission to use.
|
||||
|
||||
**Treatment:** Treat empty responses as errors informing the user that the model might be restricted or invalid for their project.
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "Token expired"
|
||||
- Refresh OAuth tokens: `picoclaw auth login --provider antigravity`
|
||||
|
||||
### "Gemini for Google Cloud is not enabled"
|
||||
- Enable the API in your Google Cloud Console
|
||||
|
||||
### "Project not found"
|
||||
- Ensure your Google Cloud project has the necessary APIs enabled
|
||||
- Check that the project ID is correctly fetched during authentication
|
||||
|
||||
### Models not appearing in list
|
||||
- Verify OAuth authentication completed successfully
|
||||
- Check auth profile storage: `~/.picoclaw/auth.json`
|
||||
- Re-run `picoclaw auth login --provider antigravity`
|
||||
@@ -0,0 +1,809 @@
|
||||
> Voltar ao [README](../project/README.pt-br.md)
|
||||
|
||||
# Guia de Autenticação e Integração do Antigravity
|
||||
|
||||
## Visão Geral
|
||||
|
||||
**Antigravity** (Google Cloud Code Assist) é um provedor de modelos de IA apoiado pelo Google que oferece acesso a modelos como Claude Opus 4.6 e Gemini através da infraestrutura de nuvem do Google. Este documento fornece um guia completo sobre como a autenticação funciona, como buscar modelos e como implementar um novo provedor no PicoClaw.
|
||||
|
||||
---
|
||||
|
||||
## Índice
|
||||
|
||||
1. [Fluxo de Autenticação](#fluxo-de-autenticação)
|
||||
2. [Detalhes da Implementação OAuth](#detalhes-da-implementação-oauth)
|
||||
3. [Gerenciamento de Tokens](#gerenciamento-de-tokens)
|
||||
4. [Busca da Lista de Modelos](#busca-da-lista-de-modelos)
|
||||
5. [Rastreamento de Uso](#rastreamento-de-uso)
|
||||
6. [Estrutura do Plugin do Provedor](#estrutura-do-plugin-do-provedor)
|
||||
7. [Requisitos de Integração](#requisitos-de-integração)
|
||||
8. [Endpoints da API](#endpoints-da-api)
|
||||
9. [Configuração](#configuração)
|
||||
10. [Criando um Novo Provedor no PicoClaw](#criando-um-novo-provedor-no-picoclaw)
|
||||
|
||||
---
|
||||
|
||||
## Fluxo de Autenticação
|
||||
|
||||
### 1. OAuth 2.0 com PKCE
|
||||
|
||||
O Antigravity utiliza **OAuth 2.0 com PKCE (Proof Key for Code Exchange)** para autenticação segura:
|
||||
|
||||
```
|
||||
┌─────────────┐ ┌─────────────────┐
|
||||
│ 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──── │ │
|
||||
└─────────────┘ └─────────────────┘
|
||||
```
|
||||
|
||||
### 2. Etapas Detalhadas
|
||||
|
||||
#### Etapa 1: Gerar Parâmetros PKCE
|
||||
```typescript
|
||||
function generatePkce(): { verifier: string; challenge: string } {
|
||||
const verifier = randomBytes(32).toString("hex");
|
||||
const challenge = createHash("sha256").update(verifier).digest("base64url");
|
||||
return { verifier, challenge };
|
||||
}
|
||||
```
|
||||
|
||||
#### Etapa 2: Construir a URL de Autorização
|
||||
```typescript
|
||||
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();
|
||||
}
|
||||
```
|
||||
|
||||
**Escopos Necessários:**
|
||||
```typescript
|
||||
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",
|
||||
];
|
||||
```
|
||||
|
||||
#### Etapa 3: Tratar o Callback OAuth
|
||||
|
||||
**Modo Automático (Desenvolvimento Local):**
|
||||
- Iniciar um servidor HTTP local na porta 51121
|
||||
- Aguardar o redirecionamento do Google
|
||||
- Extrair o código de autorização dos parâmetros da query
|
||||
|
||||
**Modo Manual (Remoto/Sem Interface Gráfica):**
|
||||
- Exibir a URL de autorização para o usuário
|
||||
- O usuário completa a autenticação no navegador
|
||||
- O usuário cola a URL de redirecionamento completa no terminal
|
||||
- Analisar o código da URL colada
|
||||
|
||||
#### Etapa 4: Trocar o Código por Tokens
|
||||
```typescript
|
||||
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
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
#### Etapa 5: Buscar Dados Adicionais do Usuário
|
||||
|
||||
**E-mail do Usuário:**
|
||||
```typescript
|
||||
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 do Projeto (Necessário para chamadas de API):**
|
||||
```typescript
|
||||
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"; // Valor padrão de fallback
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Detalhes da Implementação OAuth
|
||||
|
||||
### Credenciais do Cliente
|
||||
|
||||
**Importante:** Estas são codificadas em base64 no código-fonte para sincronização com pi-ai:
|
||||
|
||||
```typescript
|
||||
const decode = (s: string) => Buffer.from(s, "base64").toString();
|
||||
|
||||
const CLIENT_ID = decode(
|
||||
"MTA3MTAwNjA2MDU5MS10bWhzc2luMmgyMWxjcmUyMzV2dG9sb2poNGc0MDNlcC5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbQ=="
|
||||
);
|
||||
const CLIENT_SECRET = decode("R09DU1BYLUs1OEZXUjQ4NkxkTEoxbUxCOHNYQzR6NnFEQWY=");
|
||||
```
|
||||
|
||||
### Modos do Fluxo OAuth
|
||||
|
||||
1. **Fluxo Automático** (máquinas locais com navegador):
|
||||
- Abre o navegador automaticamente
|
||||
- O servidor de callback local captura o redirecionamento
|
||||
- Nenhuma interação do usuário necessária após a autenticação inicial
|
||||
|
||||
2. **Fluxo Manual** (remoto/sem interface/WSL2):
|
||||
- URL exibida para copiar e colar manualmente
|
||||
- O usuário completa a autenticação em um navegador externo
|
||||
- O usuário cola a URL de redirecionamento completa de volta
|
||||
|
||||
```typescript
|
||||
function shouldUseManualOAuthFlow(isRemote: boolean): boolean {
|
||||
return isRemote || isWSL2Sync();
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Gerenciamento de Tokens
|
||||
|
||||
### Estrutura do Perfil de Autenticação
|
||||
|
||||
```typescript
|
||||
type OAuthCredential = {
|
||||
type: "oauth";
|
||||
provider: "google-antigravity";
|
||||
access: string; // Token de acesso
|
||||
refresh: string; // Token de atualização
|
||||
expires: number; // Timestamp de expiração (ms desde epoch)
|
||||
email?: string; // E-mail do usuário
|
||||
projectId?: string; // ID do projeto Google Cloud
|
||||
};
|
||||
```
|
||||
|
||||
### Atualização de Tokens
|
||||
|
||||
A credencial inclui um token de atualização que pode ser usado para obter novos tokens de acesso quando o atual expira. A expiração é definida com um buffer de 5 minutos para evitar condições de corrida.
|
||||
|
||||
---
|
||||
|
||||
## Busca da Lista de Modelos
|
||||
|
||||
### Buscar Modelos Disponíveis
|
||||
|
||||
```typescript
|
||||
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();
|
||||
|
||||
// Retorna modelos com informações de cota
|
||||
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,
|
||||
},
|
||||
}));
|
||||
}
|
||||
```
|
||||
|
||||
### Formato da Resposta
|
||||
|
||||
```typescript
|
||||
type FetchAvailableModelsResponse = {
|
||||
models?: Record<string, {
|
||||
displayName?: string;
|
||||
quotaInfo?: {
|
||||
remainingFraction?: number | string;
|
||||
resetTime?: string; // Timestamp ISO 8601
|
||||
isExhausted?: boolean;
|
||||
};
|
||||
}>;
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Rastreamento de Uso
|
||||
|
||||
### Buscar Dados de Uso
|
||||
|
||||
```typescript
|
||||
export async function fetchAntigravityUsage(
|
||||
token: string,
|
||||
timeoutMs: number
|
||||
): Promise<ProviderUsageSnapshot> {
|
||||
// 1. Buscar créditos e informações do plano
|
||||
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",
|
||||
},
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
// Extrair informações de créditos
|
||||
const { availablePromptCredits, planInfo, currentTier } = data;
|
||||
|
||||
// 2. Buscar cotas dos modelos
|
||||
const modelsRes = await fetch(
|
||||
`${BASE_URL}/v1internal:fetchAvailableModels`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
body: JSON.stringify({ project: projectId }),
|
||||
}
|
||||
);
|
||||
|
||||
// Construir janelas de uso
|
||||
return {
|
||||
provider: "google-antigravity",
|
||||
displayName: "Google Antigravity",
|
||||
windows: [
|
||||
{ label: "Credits", usedPercent: calculateUsedPercent(available, monthly) },
|
||||
// Cotas individuais dos modelos...
|
||||
],
|
||||
plan: currentTier?.name || planType,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### Estrutura da Resposta de Uso
|
||||
|
||||
```typescript
|
||||
type ProviderUsageSnapshot = {
|
||||
provider: "google-antigravity";
|
||||
displayName: string;
|
||||
windows: UsageWindow[];
|
||||
plan?: string;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
type UsageWindow = {
|
||||
label: string; // "Credits" ou ID do modelo
|
||||
usedPercent: number; // 0-100
|
||||
resetAt?: number; // Timestamp de quando a cota é redefinida
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Estrutura do Plugin do Provedor
|
||||
|
||||
### Definição do Plugin
|
||||
|
||||
```typescript
|
||||
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) => {
|
||||
// Implementação OAuth aqui
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
### ProviderAuthContext
|
||||
|
||||
```typescript
|
||||
type ProviderAuthContext = {
|
||||
config: PicoClawConfig;
|
||||
agentDir?: string;
|
||||
workspaceDir?: string;
|
||||
prompter: WizardPrompter; // Prompts/notificações da UI
|
||||
runtime: RuntimeEnv; // Logging, etc.
|
||||
isRemote: boolean; // Se está executando remotamente
|
||||
openUrl: (url: string) => Promise<void>; // Abridor de navegador
|
||||
oauth: {
|
||||
createVpsAwareHandlers: Function;
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
### ProviderAuthResult
|
||||
|
||||
```typescript
|
||||
type ProviderAuthResult = {
|
||||
profiles: Array<{
|
||||
profileId: string;
|
||||
credential: AuthProfileCredential;
|
||||
}>;
|
||||
configPatch?: Partial<PicoClawConfig>;
|
||||
defaultModel?: string;
|
||||
notes?: string[];
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Requisitos de Integração
|
||||
|
||||
### 1. Ambiente/Dependências Necessários
|
||||
|
||||
- Go ≥ 1.25
|
||||
- Base de código do PicoClaw (`pkg/providers/` e `pkg/auth/`)
|
||||
- Pacotes da biblioteca padrão `crypto` e `net/http`
|
||||
|
||||
### 2. Cabeçalhos Necessários para Chamadas de API
|
||||
|
||||
```typescript
|
||||
const 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",
|
||||
};
|
||||
|
||||
// Para chamadas loadCodeAssist, incluir também:
|
||||
const CLIENT_METADATA = {
|
||||
ideType: "ANTIGRAVITY", // ou "IDE_UNSPECIFIED"
|
||||
platform: "PLATFORM_UNSPECIFIED",
|
||||
pluginType: "GEMINI",
|
||||
};
|
||||
```
|
||||
|
||||
### 3. Sanitização de Schemas de Modelos
|
||||
|
||||
O Antigravity usa modelos compatíveis com Gemini, então os schemas de ferramentas devem ser sanitizados:
|
||||
|
||||
```typescript
|
||||
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",
|
||||
]);
|
||||
|
||||
// Limpar schema antes de enviar
|
||||
function cleanToolSchemaForGemini(schema: Record<string, unknown>): unknown {
|
||||
// Remover palavras-chave não suportadas
|
||||
// Garantir que o nível superior tenha type: "object"
|
||||
// Achatar uniões anyOf/oneOf
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Tratamento de Blocos de Pensamento (Modelos Claude)
|
||||
|
||||
Para modelos Claude via Antigravity, os blocos de pensamento requerem tratamento especial:
|
||||
|
||||
```typescript
|
||||
const ANTIGRAVITY_SIGNATURE_RE = /^[A-Za-z0-9+/]+={0,2}$/;
|
||||
|
||||
export function sanitizeAntigravityThinkingBlocks(
|
||||
messages: AgentMessage[]
|
||||
): AgentMessage[] {
|
||||
// Validar assinaturas de pensamento
|
||||
// Normalizar campos de assinatura
|
||||
// Descartar blocos de pensamento não assinados
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Endpoints da API
|
||||
|
||||
### Endpoints de Autenticação
|
||||
|
||||
| Endpoint | Método | Finalidade |
|
||||
|----------|--------|-----------|
|
||||
| `https://accounts.google.com/o/oauth2/v2/auth` | GET | Autorização OAuth |
|
||||
| `https://oauth2.googleapis.com/token` | POST | Troca de tokens |
|
||||
| `https://www.googleapis.com/oauth2/v1/userinfo` | GET | Informações do usuário (e-mail) |
|
||||
|
||||
### Endpoints do Cloud Code Assist
|
||||
|
||||
| Endpoint | Método | Finalidade |
|
||||
|----------|--------|-----------|
|
||||
| `https://cloudcode-pa.googleapis.com/v1internal:loadCodeAssist` | POST | Carregar informações do projeto, créditos, plano |
|
||||
| `https://cloudcode-pa.googleapis.com/v1internal:fetchAvailableModels` | POST | Listar modelos disponíveis com cotas |
|
||||
| `https://cloudcode-pa.googleapis.com/v1internal:streamGenerateContent?alt=sse` | POST | Endpoint de streaming de chat |
|
||||
|
||||
**Formato de Requisição da API (Chat):**
|
||||
O endpoint `v1internal:streamGenerateContent` espera um envelope encapsulando a requisição Gemini padrão:
|
||||
|
||||
```json
|
||||
{
|
||||
"project": "your-project-id",
|
||||
"model": "model-id",
|
||||
"request": {
|
||||
"contents": [...],
|
||||
"systemInstruction": {...},
|
||||
"generationConfig": {...},
|
||||
"tools": [...]
|
||||
},
|
||||
"requestType": "agent",
|
||||
"userAgent": "antigravity",
|
||||
"requestId": "agent-timestamp-random"
|
||||
}
|
||||
```
|
||||
|
||||
**Formato de Resposta da API (SSE):**
|
||||
Cada mensagem SSE (`data: {...}`) é encapsulada em um campo `response`:
|
||||
|
||||
```json
|
||||
{
|
||||
"response": {
|
||||
"candidates": [...],
|
||||
"usageMetadata": {...},
|
||||
"modelVersion": "...",
|
||||
"responseId": "..."
|
||||
},
|
||||
"traceId": "...",
|
||||
"metadata": {}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Configuração
|
||||
|
||||
### Configuração do config.json
|
||||
|
||||
```json
|
||||
{
|
||||
"model_list": [
|
||||
{
|
||||
"model_name": "gemini-flash",
|
||||
"model": "antigravity/gemini-3-flash",
|
||||
"auth_method": "oauth"
|
||||
}
|
||||
],
|
||||
"agents": {
|
||||
"defaults": {
|
||||
"model_name": "gemini-flash"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Armazenamento do Perfil de Autenticação
|
||||
|
||||
Os perfis de autenticação são armazenados em `~/.picoclaw/auth.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"credentials": {
|
||||
"google-antigravity": {
|
||||
"access_token": "ya29...",
|
||||
"refresh_token": "1//...",
|
||||
"expires_at": "2026-01-01T00:00:00Z",
|
||||
"provider": "google-antigravity",
|
||||
"auth_method": "oauth",
|
||||
"email": "user@example.com",
|
||||
"project_id": "my-project-id"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Criando um Novo Provedor no PicoClaw
|
||||
|
||||
Os provedores do PicoClaw são implementados como pacotes Go em `pkg/providers/`. Para adicionar um novo provedor:
|
||||
|
||||
### Implementação Passo a Passo
|
||||
|
||||
#### 1. Criar o Arquivo do Provedor
|
||||
|
||||
Crie um novo arquivo Go em `pkg/providers/`:
|
||||
|
||||
```
|
||||
pkg/providers/
|
||||
└── your_provider.go
|
||||
```
|
||||
|
||||
#### 2. Implementar a Interface Provider
|
||||
|
||||
Seu provedor deve implementar a interface `Provider` definida em `pkg/providers/types.go`:
|
||||
|
||||
```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 {
|
||||
// Implementar conclusão de chat com streaming
|
||||
}
|
||||
```
|
||||
|
||||
#### 3. Registrar na Factory
|
||||
|
||||
Adicione seu provedor ao switch de protocolo em `pkg/providers/factory.go`:
|
||||
|
||||
```go
|
||||
case "your-provider":
|
||||
return NewYourProvider(sel.apiKey, sel.apiBase, sel.proxy), nil
|
||||
```
|
||||
|
||||
#### 4. Adicionar Configuração Padrão (Opcional)
|
||||
|
||||
Adicione uma entrada padrão em `pkg/config/defaults.go`:
|
||||
|
||||
```go
|
||||
{
|
||||
ModelName: "your-model",
|
||||
Model: "your-provider/model-name",
|
||||
APIKey: "",
|
||||
},
|
||||
```
|
||||
|
||||
#### 5. Adicionar Suporte de Autenticação (Opcional)
|
||||
|
||||
Se seu provedor requer OAuth ou autenticação especial, adicione um caso em `cmd/picoclaw/internal/auth/helpers.go`:
|
||||
|
||||
```go
|
||||
case "your-provider":
|
||||
authLoginYourProvider()
|
||||
```
|
||||
|
||||
#### 6. Configurar via `config.json`
|
||||
|
||||
```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"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testando Sua Implementação
|
||||
|
||||
### Comandos CLI
|
||||
|
||||
```bash
|
||||
# Autenticar com um provedor
|
||||
picoclaw auth login --provider your-provider
|
||||
|
||||
# Listar modelos (para Antigravity)
|
||||
picoclaw auth models
|
||||
|
||||
# Iniciar o gateway
|
||||
picoclaw gateway
|
||||
|
||||
# Executar um agente com um modelo específico
|
||||
picoclaw agent -m "Hello" --model your-model
|
||||
```
|
||||
|
||||
### Variáveis de Ambiente para Testes
|
||||
|
||||
```bash
|
||||
# Substituir o modelo padrão
|
||||
export PICOCLAW_AGENTS_DEFAULTS_MODEL=your-model
|
||||
|
||||
# Substituir configurações do provedor
|
||||
export PICOCLAW_MODEL_LIST='[{"model_name":"your-model","model":"your-provider/model-name","api_key":"..."}]'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Referências
|
||||
|
||||
- **Arquivos Fonte:**
|
||||
- `pkg/providers/antigravity_provider.go` - Implementação do provedor Antigravity
|
||||
- `pkg/auth/oauth.go` - Implementação do fluxo OAuth
|
||||
- `pkg/auth/store.go` - Armazenamento de credenciais de autenticação (`~/.picoclaw/auth.json`)
|
||||
- `pkg/providers/factory.go` - Factory de provedores e roteamento de protocolo
|
||||
- `pkg/providers/types.go` - Definições da interface do provedor
|
||||
- `cmd/picoclaw/internal/auth/helpers.go` - Comandos CLI de autenticação
|
||||
|
||||
- **Documentação:**
|
||||
- `docs/ANTIGRAVITY_USAGE.md` - Guia de uso do Antigravity
|
||||
- `docs/migration/model-list-migration.md` - Guia de migração
|
||||
|
||||
---
|
||||
|
||||
## Observações
|
||||
|
||||
1. **Projeto Google Cloud:** O Antigravity requer que o Gemini for Google Cloud esteja habilitado no seu projeto Google Cloud
|
||||
2. **Cotas:** Usa cotas do projeto Google Cloud (sem cobrança separada)
|
||||
3. **Acesso a Modelos:** Os modelos disponíveis dependem da configuração do seu projeto Google Cloud
|
||||
4. **Blocos de Pensamento:** Modelos Claude via Antigravity requerem tratamento especial de blocos de pensamento com assinaturas
|
||||
5. **Sanitização de Schemas:** Os schemas de ferramentas devem ser sanitizados para remover palavras-chave JSON Schema não suportadas
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
## Tratamento de Erros Comuns
|
||||
|
||||
### 1. Limitação de Taxa (HTTP 429)
|
||||
|
||||
O Antigravity retorna um erro 429 quando as cotas do projeto/modelo estão esgotadas. A resposta de erro frequentemente contém um `quotaResetDelay` no campo `details`.
|
||||
|
||||
**Exemplo de Erro 429:**
|
||||
```json
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Respostas Vazias (Modelos Restritos)
|
||||
|
||||
Alguns modelos podem aparecer na lista de modelos disponíveis, mas retornar uma resposta vazia (200 OK mas stream SSE vazio). Isso geralmente acontece com modelos em preview ou restritos que o projeto atual não tem permissão para usar.
|
||||
|
||||
**Tratamento:** Tratar respostas vazias como erros informando ao usuário que o modelo pode estar restrito ou inválido para seu projeto.
|
||||
|
||||
---
|
||||
|
||||
## Solução de Problemas
|
||||
|
||||
### "Token expired" (token expirado)
|
||||
- Atualizar tokens OAuth: `picoclaw auth login --provider antigravity`
|
||||
|
||||
### "Gemini for Google Cloud is not enabled" (Gemini for Google Cloud não está habilitado)
|
||||
- Habilitar a API no seu Google Cloud Console
|
||||
|
||||
### "Project not found" (projeto não encontrado)
|
||||
- Verificar se seu projeto Google Cloud tem as APIs necessárias habilitadas
|
||||
- Verificar se o ID do projeto foi obtido corretamente durante a autenticação
|
||||
|
||||
### Modelos não aparecem na lista
|
||||
- Verificar se a autenticação OAuth foi concluída com sucesso
|
||||
- Verificar o armazenamento do perfil de autenticação: `~/.picoclaw/auth.json`
|
||||
- Executar novamente `picoclaw auth login --provider antigravity`
|
||||
@@ -0,0 +1,807 @@
|
||||
> Quay lại [README](../project/README.vi.md)
|
||||
|
||||
# Hướng dẫn Xác thực và Tích hợp Antigravity
|
||||
|
||||
## Tổng quan
|
||||
|
||||
**Antigravity** (Google Cloud Code Assist) là nhà cung cấp mô hình AI được Google hỗ trợ, cung cấp quyền truy cập vào các mô hình như Claude Opus 4.6 và Gemini thông qua hạ tầng đám mây của Google. Tài liệu này cung cấp hướng dẫn đầy đủ về cách xác thực hoạt động, cách lấy danh sách mô hình và cách triển khai nhà cung cấp mới trong PicoClaw.
|
||||
|
||||
---
|
||||
|
||||
## Mục lục
|
||||
|
||||
1. [Luồng xác thực](#luồng-xác-thực)
|
||||
2. [Chi tiết triển khai OAuth](#chi-tiết-triển-khai-oauth)
|
||||
3. [Quản lý token](#quản-lý-token)
|
||||
4. [Lấy danh sách mô hình](#lấy-danh-sách-mô-hình)
|
||||
5. [Theo dõi mức sử dụng](#theo-dõi-mức-sử-dụng)
|
||||
6. [Cấu trúc plugin nhà cung cấp](#cấu-trúc-plugin-nhà-cung-cấp)
|
||||
7. [Yêu cầu tích hợp](#yêu-cầu-tích-hợp)
|
||||
8. [Các endpoint API](#các-endpoint-api)
|
||||
9. [Cấu hình](#cấu-hình)
|
||||
10. [Tạo nhà cung cấp mới trong PicoClaw](#tạo-nhà-cung-cấp-mới-trong-picoclaw)
|
||||
|
||||
---
|
||||
|
||||
## Luồng xác thực
|
||||
|
||||
### 1. OAuth 2.0 với PKCE
|
||||
|
||||
Antigravity sử dụng **OAuth 2.0 với PKCE (Proof Key for Code Exchange)** để xác thực an toàn:
|
||||
|
||||
```
|
||||
┌─────────────┐ ┌─────────────────┐
|
||||
│ 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──── │ │
|
||||
└─────────────┘ └─────────────────┘
|
||||
```
|
||||
|
||||
### 2. Các bước chi tiết
|
||||
|
||||
#### Bước 1: Tạo tham số PKCE
|
||||
```typescript
|
||||
function generatePkce(): { verifier: string; challenge: string } {
|
||||
const verifier = randomBytes(32).toString("hex");
|
||||
const challenge = createHash("sha256").update(verifier).digest("base64url");
|
||||
return { verifier, challenge };
|
||||
}
|
||||
```
|
||||
|
||||
#### Bước 2: Xây dựng URL ủy quyền
|
||||
```typescript
|
||||
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();
|
||||
}
|
||||
```
|
||||
|
||||
**Các phạm vi quyền cần thiết:**
|
||||
```typescript
|
||||
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",
|
||||
];
|
||||
```
|
||||
|
||||
#### Bước 3: Xử lý callback OAuth
|
||||
|
||||
**Chế độ tự động (Phát triển cục bộ):**
|
||||
- Khởi động máy chủ HTTP cục bộ trên cổng 51121
|
||||
- Chờ chuyển hướng từ Google
|
||||
- Trích xuất mã ủy quyền từ tham số truy vấn
|
||||
|
||||
**Chế độ thủ công (Từ xa/Không có giao diện):**
|
||||
- Hiển thị URL ủy quyền cho người dùng
|
||||
- Người dùng hoàn tất xác thực trong trình duyệt
|
||||
- Người dùng dán URL chuyển hướng đầy đủ vào terminal
|
||||
- Phân tích mã từ URL đã dán
|
||||
|
||||
#### Bước 4: Đổi mã lấy token
|
||||
```typescript
|
||||
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
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
#### Bước 5: Lấy dữ liệu người dùng bổ sung
|
||||
|
||||
**Email người dùng:**
|
||||
```typescript
|
||||
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 dự án (Bắt buộc cho các lệnh gọi API):**
|
||||
```typescript
|
||||
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"; // Giá trị mặc định dự phòng
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Chi tiết triển khai OAuth
|
||||
|
||||
### Thông tin xác thực client
|
||||
|
||||
**Quan trọng:** Các giá trị này được mã hóa base64 trong mã nguồn để đồng bộ với pi-ai:
|
||||
|
||||
```typescript
|
||||
const decode = (s: string) => Buffer.from(s, "base64").toString();
|
||||
|
||||
const CLIENT_ID = decode(
|
||||
"MTA3MTAwNjA2MDU5MS10bWhzc2luMmgyMWxjcmUyMzV2dG9sb2poNGc0MDNlcC5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbQ=="
|
||||
);
|
||||
const CLIENT_SECRET = decode("R09DU1BYLUs1OEZXUjQ4NkxkTEoxbUxCOHNYQzR6NnFEQWY=");
|
||||
```
|
||||
|
||||
### Các chế độ luồng OAuth
|
||||
|
||||
1. **Luồng tự động** (Máy cục bộ có trình duyệt):
|
||||
- Tự động mở trình duyệt
|
||||
- Máy chủ callback cục bộ bắt chuyển hướng
|
||||
- Không cần tương tác người dùng sau xác thực ban đầu
|
||||
|
||||
2. **Luồng thủ công** (Từ xa/Không có giao diện/WSL2):
|
||||
- Hiển thị URL để sao chép-dán thủ công
|
||||
- Người dùng hoàn tất xác thực trong trình duyệt bên ngoài
|
||||
- Người dùng dán lại URL chuyển hướng đầy đủ
|
||||
|
||||
```typescript
|
||||
function shouldUseManualOAuthFlow(isRemote: boolean): boolean {
|
||||
return isRemote || isWSL2Sync();
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quản lý token
|
||||
|
||||
### Cấu trúc hồ sơ xác thực
|
||||
|
||||
```typescript
|
||||
type OAuthCredential = {
|
||||
type: "oauth";
|
||||
provider: "google-antigravity";
|
||||
access: string; // Token truy cập
|
||||
refresh: string; // Token làm mới
|
||||
expires: number; // Dấu thời gian hết hạn (ms kể từ epoch)
|
||||
email?: string; // Email người dùng
|
||||
projectId?: string; // ID dự án Google Cloud
|
||||
};
|
||||
```
|
||||
|
||||
### Làm mới token
|
||||
|
||||
Thông tin xác thực bao gồm token làm mới có thể được sử dụng để lấy token truy cập mới khi token hiện tại hết hạn. Thời gian hết hạn được đặt với bộ đệm 5 phút để tránh điều kiện tranh chấp.
|
||||
|
||||
---
|
||||
|
||||
## Lấy danh sách mô hình
|
||||
|
||||
### Lấy các mô hình khả dụng
|
||||
|
||||
```typescript
|
||||
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();
|
||||
|
||||
// Trả về các mô hình kèm thông tin hạn mức
|
||||
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,
|
||||
},
|
||||
}));
|
||||
}
|
||||
```
|
||||
|
||||
### Định dạng phản hồi
|
||||
|
||||
```typescript
|
||||
type FetchAvailableModelsResponse = {
|
||||
models?: Record<string, {
|
||||
displayName?: string;
|
||||
quotaInfo?: {
|
||||
remainingFraction?: number | string;
|
||||
resetTime?: string; // Dấu thời gian ISO 8601
|
||||
isExhausted?: boolean;
|
||||
};
|
||||
}>;
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Theo dõi mức sử dụng
|
||||
|
||||
### Lấy dữ liệu sử dụng
|
||||
|
||||
```typescript
|
||||
export async function fetchAntigravityUsage(
|
||||
token: string,
|
||||
timeoutMs: number
|
||||
): Promise<ProviderUsageSnapshot> {
|
||||
// 1. Lấy thông tin tín dụng và gói dịch vụ
|
||||
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",
|
||||
},
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
// Trích xuất thông tin tín dụng
|
||||
const { availablePromptCredits, planInfo, currentTier } = data;
|
||||
|
||||
// 2. Lấy hạn mức mô hình
|
||||
const modelsRes = await fetch(
|
||||
`${BASE_URL}/v1internal:fetchAvailableModels`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
body: JSON.stringify({ project: projectId }),
|
||||
}
|
||||
);
|
||||
|
||||
// Xây dựng cửa sổ sử dụng
|
||||
return {
|
||||
provider: "google-antigravity",
|
||||
displayName: "Google Antigravity",
|
||||
windows: [
|
||||
{ label: "Credits", usedPercent: calculateUsedPercent(available, monthly) },
|
||||
// Hạn mức từng mô hình...
|
||||
],
|
||||
plan: currentTier?.name || planType,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### Cấu trúc phản hồi sử dụng
|
||||
|
||||
```typescript
|
||||
type ProviderUsageSnapshot = {
|
||||
provider: "google-antigravity";
|
||||
displayName: string;
|
||||
windows: UsageWindow[];
|
||||
plan?: string;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
type UsageWindow = {
|
||||
label: string; // "Credits" hoặc ID mô hình
|
||||
usedPercent: number; // 0-100
|
||||
resetAt?: number; // Dấu thời gian khi hạn mức được đặt lại
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Cấu trúc plugin nhà cung cấp
|
||||
|
||||
### Định nghĩa plugin
|
||||
|
||||
```typescript
|
||||
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) => {
|
||||
// Triển khai OAuth tại đây
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
### ProviderAuthContext
|
||||
|
||||
```typescript
|
||||
type ProviderAuthContext = {
|
||||
config: PicoClawConfig;
|
||||
agentDir?: string;
|
||||
workspaceDir?: string;
|
||||
prompter: WizardPrompter; // Lời nhắc/thông báo UI
|
||||
runtime: RuntimeEnv; // Ghi log, v.v.
|
||||
isRemote: boolean; // Có đang chạy từ xa không
|
||||
openUrl: (url: string) => Promise<void>; // Mở trình duyệt
|
||||
oauth: {
|
||||
createVpsAwareHandlers: Function;
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
### ProviderAuthResult
|
||||
|
||||
```typescript
|
||||
type ProviderAuthResult = {
|
||||
profiles: Array<{
|
||||
profileId: string;
|
||||
credential: AuthProfileCredential;
|
||||
}>;
|
||||
configPatch?: Partial<PicoClawConfig>;
|
||||
defaultModel?: string;
|
||||
notes?: string[];
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Yêu cầu tích hợp
|
||||
|
||||
### 1. Môi trường/Phụ thuộc cần thiết
|
||||
|
||||
- Go ≥ 1.25
|
||||
- Mã nguồn PicoClaw (`pkg/providers/` và `pkg/auth/`)
|
||||
- Các gói thư viện chuẩn `crypto` và `net/http`
|
||||
|
||||
### 2. Các header bắt buộc cho lệnh gọi API
|
||||
|
||||
```typescript
|
||||
const REQUIRED_HEADERS = {
|
||||
"Authorization": `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": "antigravity", // hoặc "google-api-nodejs-client/9.15.1"
|
||||
"X-Goog-Api-Client": "google-cloud-sdk vscode_cloudshelleditor/0.1",
|
||||
};
|
||||
|
||||
// Đối với các lệnh gọi loadCodeAssist, cũng bao gồm:
|
||||
const CLIENT_METADATA = {
|
||||
ideType: "ANTIGRAVITY", // hoặc "IDE_UNSPECIFIED"
|
||||
platform: "PLATFORM_UNSPECIFIED",
|
||||
pluginType: "GEMINI",
|
||||
};
|
||||
```
|
||||
|
||||
### 3. Làm sạch schema mô hình
|
||||
|
||||
Antigravity sử dụng các mô hình tương thích Gemini, vì vậy schema công cụ phải được làm sạch:
|
||||
|
||||
```typescript
|
||||
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",
|
||||
]);
|
||||
|
||||
// Làm sạch schema trước khi gửi
|
||||
function cleanToolSchemaForGemini(schema: Record<string, unknown>): unknown {
|
||||
// Xóa các từ khóa không được hỗ trợ
|
||||
// Đảm bảo cấp cao nhất có type: "object"
|
||||
// Làm phẳng các union anyOf/oneOf
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Xử lý khối suy nghĩ (Mô hình Claude)
|
||||
|
||||
Đối với các mô hình Claude qua Antigravity, khối suy nghĩ cần xử lý đặc biệt:
|
||||
|
||||
```typescript
|
||||
const ANTIGRAVITY_SIGNATURE_RE = /^[A-Za-z0-9+/]+={0,2}$/;
|
||||
|
||||
export function sanitizeAntigravityThinkingBlocks(
|
||||
messages: AgentMessage[]
|
||||
): AgentMessage[] {
|
||||
// Xác thực chữ ký suy nghĩ
|
||||
// Chuẩn hóa các trường chữ ký
|
||||
// Loại bỏ các khối suy nghĩ chưa ký
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Các endpoint API
|
||||
|
||||
### Endpoint xác thực
|
||||
|
||||
| Endpoint | Phương thức | Mục đích |
|
||||
|----------|------------|----------|
|
||||
| `https://accounts.google.com/o/oauth2/v2/auth` | GET | Ủy quyền OAuth |
|
||||
| `https://oauth2.googleapis.com/token` | POST | Trao đổi token |
|
||||
| `https://www.googleapis.com/oauth2/v1/userinfo` | GET | Thông tin người dùng (email) |
|
||||
|
||||
### Endpoint Cloud Code Assist
|
||||
|
||||
| Endpoint | Phương thức | Mục đích |
|
||||
|----------|------------|----------|
|
||||
| `https://cloudcode-pa.googleapis.com/v1internal:loadCodeAssist` | POST | Tải thông tin dự án, tín dụng, gói dịch vụ |
|
||||
| `https://cloudcode-pa.googleapis.com/v1internal:fetchAvailableModels` | POST | Liệt kê các mô hình khả dụng kèm hạn mức |
|
||||
| `https://cloudcode-pa.googleapis.com/v1internal:streamGenerateContent?alt=sse` | POST | Endpoint streaming chat |
|
||||
|
||||
**Định dạng yêu cầu API (Chat):**
|
||||
Endpoint `v1internal:streamGenerateContent` yêu cầu một envelope bao bọc yêu cầu Gemini tiêu chuẩn:
|
||||
|
||||
```json
|
||||
{
|
||||
"project": "your-project-id",
|
||||
"model": "model-id",
|
||||
"request": {
|
||||
"contents": [...],
|
||||
"systemInstruction": {...},
|
||||
"generationConfig": {...},
|
||||
"tools": [...]
|
||||
},
|
||||
"requestType": "agent",
|
||||
"userAgent": "antigravity",
|
||||
"requestId": "agent-timestamp-random"
|
||||
}
|
||||
```
|
||||
|
||||
**Định dạng phản hồi API (SSE):**
|
||||
Mỗi thông điệp SSE (`data: {...}`) được bao bọc trong trường `response`:
|
||||
|
||||
```json
|
||||
{
|
||||
"response": {
|
||||
"candidates": [...],
|
||||
"usageMetadata": {...},
|
||||
"modelVersion": "...",
|
||||
"responseId": "..."
|
||||
},
|
||||
"traceId": "...",
|
||||
"metadata": {}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Cấu hình
|
||||
|
||||
### Cấu hình config.json
|
||||
|
||||
```json
|
||||
{
|
||||
"model_list": [
|
||||
{
|
||||
"model_name": "gemini-flash",
|
||||
"model": "antigravity/gemini-3-flash",
|
||||
"auth_method": "oauth"
|
||||
}
|
||||
],
|
||||
"agents": {
|
||||
"defaults": {
|
||||
"model_name": "gemini-flash"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Lưu trữ hồ sơ xác thực
|
||||
|
||||
Hồ sơ xác thực được lưu trữ trong `~/.picoclaw/auth.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"credentials": {
|
||||
"google-antigravity": {
|
||||
"access_token": "ya29...",
|
||||
"refresh_token": "1//...",
|
||||
"expires_at": "2026-01-01T00:00:00Z",
|
||||
"provider": "google-antigravity",
|
||||
"auth_method": "oauth",
|
||||
"email": "user@example.com",
|
||||
"project_id": "my-project-id"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Tạo nhà cung cấp mới trong PicoClaw
|
||||
|
||||
Các nhà cung cấp PicoClaw được triển khai dưới dạng gói Go trong `pkg/providers/`. Để thêm nhà cung cấp mới:
|
||||
|
||||
### Triển khai từng bước
|
||||
|
||||
#### 1. Tạo file nhà cung cấp
|
||||
|
||||
Tạo file Go mới trong `pkg/providers/`:
|
||||
|
||||
```
|
||||
pkg/providers/
|
||||
└── your_provider.go
|
||||
```
|
||||
|
||||
#### 2. Triển khai interface Provider
|
||||
|
||||
Nhà cung cấp của bạn phải triển khai interface `Provider` được định nghĩa trong `pkg/providers/types.go`:
|
||||
|
||||
```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 {
|
||||
// Triển khai hoàn thành chat với streaming
|
||||
}
|
||||
```
|
||||
|
||||
#### 3. Đăng ký trong factory
|
||||
|
||||
Thêm nhà cung cấp của bạn vào switch giao thức trong `pkg/providers/factory.go`:
|
||||
|
||||
```go
|
||||
case "your-provider":
|
||||
return NewYourProvider(sel.apiKey, sel.apiBase, sel.proxy), nil
|
||||
```
|
||||
|
||||
#### 4. Thêm cấu hình mặc định (Tùy chọn)
|
||||
|
||||
Thêm mục mặc định trong `pkg/config/defaults.go`:
|
||||
|
||||
```go
|
||||
{
|
||||
ModelName: "your-model",
|
||||
Model: "your-provider/model-name",
|
||||
APIKey: "",
|
||||
},
|
||||
```
|
||||
|
||||
#### 5. Thêm hỗ trợ xác thực (Tùy chọn)
|
||||
|
||||
Nếu nhà cung cấp của bạn yêu cầu OAuth hoặc xác thực đặc biệt, thêm case vào `cmd/picoclaw/internal/auth/helpers.go`:
|
||||
|
||||
```go
|
||||
case "your-provider":
|
||||
authLoginYourProvider()
|
||||
```
|
||||
|
||||
#### 6. Cấu hình qua `config.json`
|
||||
|
||||
```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"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Kiểm thử triển khai của bạn
|
||||
|
||||
### Lệnh CLI
|
||||
|
||||
```bash
|
||||
# Xác thực với nhà cung cấp
|
||||
picoclaw auth login --provider your-provider
|
||||
|
||||
# Liệt kê mô hình (cho Antigravity)
|
||||
picoclaw auth models
|
||||
|
||||
# Khởi động gateway
|
||||
picoclaw gateway
|
||||
|
||||
# Chạy agent với mô hình cụ thể
|
||||
picoclaw agent -m "Hello" --model your-model
|
||||
```
|
||||
|
||||
### Biến môi trường cho kiểm thử
|
||||
|
||||
```bash
|
||||
# Ghi đè mô hình mặc định
|
||||
export PICOCLAW_AGENTS_DEFAULTS_MODEL=your-model
|
||||
|
||||
# Ghi đè cài đặt nhà cung cấp
|
||||
export PICOCLAW_MODEL_LIST='[{"model_name":"your-model","model":"your-provider/model-name","api_key":"..."}]'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Tài liệu tham khảo
|
||||
|
||||
- **File nguồn:**
|
||||
- `pkg/providers/antigravity_provider.go` - Triển khai nhà cung cấp Antigravity
|
||||
- `pkg/auth/oauth.go` - Triển khai luồng OAuth
|
||||
- `pkg/auth/store.go` - Lưu trữ thông tin xác thực (`~/.picoclaw/auth.json`)
|
||||
- `pkg/providers/factory.go` - Factory nhà cung cấp và định tuyến giao thức
|
||||
- `pkg/providers/types.go` - Định nghĩa interface nhà cung cấp
|
||||
- `cmd/picoclaw/internal/auth/helpers.go` - Lệnh CLI xác thực
|
||||
|
||||
- **Tài liệu:**
|
||||
- `docs/ANTIGRAVITY_USAGE.md` - Hướng dẫn sử dụng Antigravity
|
||||
- `docs/migration/model-list-migration.md` - Hướng dẫn di chuyển
|
||||
|
||||
---
|
||||
|
||||
## Lưu ý
|
||||
|
||||
1. **Dự án Google Cloud:** Antigravity yêu cầu Gemini for Google Cloud được bật trên dự án Google Cloud của bạn
|
||||
2. **Hạn mức:** Sử dụng hạn mức dự án Google Cloud (không tính phí riêng)
|
||||
3. **Truy cập mô hình:** Các mô hình khả dụng phụ thuộc vào cấu hình dự án Google Cloud của bạn
|
||||
4. **Khối suy nghĩ:** Mô hình Claude qua Antigravity yêu cầu xử lý đặc biệt khối suy nghĩ có chữ ký
|
||||
5. **Làm sạch schema:** Schema công cụ phải được làm sạch để loại bỏ các từ khóa JSON Schema không được hỗ trợ
|
||||
|
||||
---
|
||||
|
||||
## Xử lý lỗi thường gặp
|
||||
|
||||
### 1. Giới hạn tốc độ (HTTP 429)
|
||||
|
||||
Antigravity trả về lỗi 429 khi hạn mức dự án/mô hình đã cạn kiệt. Phản hồi lỗi thường chứa `quotaResetDelay` trong trường `details`.
|
||||
|
||||
**Ví dụ lỗi 429:**
|
||||
```json
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Phản hồi trống (Mô hình bị hạn chế)
|
||||
|
||||
Một số mô hình có thể xuất hiện trong danh sách mô hình khả dụng nhưng trả về phản hồi trống (200 OK nhưng luồng SSE trống). Điều này thường xảy ra với các mô hình xem trước hoặc bị hạn chế mà dự án hiện tại không có quyền sử dụng.
|
||||
|
||||
**Cách xử lý:** Coi phản hồi trống là lỗi, thông báo cho người dùng rằng mô hình có thể bị hạn chế hoặc không hợp lệ cho dự án của họ.
|
||||
|
||||
---
|
||||
|
||||
## Khắc phục sự cố
|
||||
|
||||
### "Token expired" (Token đã hết hạn)
|
||||
- Làm mới token OAuth: `picoclaw auth login --provider antigravity`
|
||||
|
||||
### "Gemini for Google Cloud is not enabled" (Gemini for Google Cloud chưa được bật)
|
||||
- Bật API trong Google Cloud Console của bạn
|
||||
|
||||
### "Project not found" (Không tìm thấy dự án)
|
||||
- Đảm bảo dự án Google Cloud của bạn đã bật các API cần thiết
|
||||
- Kiểm tra xem ID dự án có được lấy chính xác trong quá trình xác thực không
|
||||
|
||||
### Mô hình không xuất hiện trong danh sách
|
||||
- Xác minh xác thực OAuth đã hoàn tất thành công
|
||||
- Kiểm tra lưu trữ hồ sơ xác thực: `~/.picoclaw/auth.json`
|
||||
- Chạy lại `picoclaw auth login --provider antigravity`
|
||||
@@ -0,0 +1,809 @@
|
||||
> 返回 [README](../project/README.zh.md)
|
||||
|
||||
# Antigravity 认证与集成指南
|
||||
|
||||
## 概述
|
||||
|
||||
**Antigravity**(Google Cloud Code Assist)是由 Google 支持的 AI 模型提供商,通过 Google 的云基础设施提供对 Claude Opus 4.6 和 Gemini 等模型的访问。本文档提供了关于认证工作原理、如何获取模型以及如何在 PicoClaw 中实现新提供商的完整指南。
|
||||
|
||||
---
|
||||
|
||||
## 目录
|
||||
|
||||
1. [认证流程](#认证流程)
|
||||
2. [OAuth 实现细节](#oauth-实现细节)
|
||||
3. [令牌管理](#令牌管理)
|
||||
4. [模型列表获取](#模型列表获取)
|
||||
5. [用量追踪](#用量追踪)
|
||||
6. [提供商插件结构](#提供商插件结构)
|
||||
7. [集成要求](#集成要求)
|
||||
8. [API 端点](#api-端点)
|
||||
9. [配置](#配置)
|
||||
10. [在 PicoClaw 中创建新提供商](#在-picoclaw-中创建新提供商)
|
||||
|
||||
---
|
||||
|
||||
## 认证流程
|
||||
|
||||
### 1. 带 PKCE 的 OAuth 2.0
|
||||
|
||||
Antigravity 使用 **OAuth 2.0 with PKCE(Proof Key for Code Exchange)** 进行安全认证:
|
||||
|
||||
```
|
||||
┌─────────────┐ ┌─────────────────┐
|
||||
│ 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──── │ │
|
||||
└─────────────┘ └─────────────────┘
|
||||
```
|
||||
|
||||
### 2. 详细步骤
|
||||
|
||||
#### 步骤 1:生成 PKCE 参数
|
||||
```typescript
|
||||
function generatePkce(): { verifier: string; challenge: string } {
|
||||
const verifier = randomBytes(32).toString("hex");
|
||||
const challenge = createHash("sha256").update(verifier).digest("base64url");
|
||||
return { verifier, challenge };
|
||||
}
|
||||
```
|
||||
|
||||
#### 步骤 2:构建授权 URL
|
||||
```typescript
|
||||
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();
|
||||
}
|
||||
```
|
||||
|
||||
**所需权限范围:**
|
||||
```typescript
|
||||
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",
|
||||
];
|
||||
```
|
||||
|
||||
#### 步骤 3:处理 OAuth 回调
|
||||
|
||||
**自动模式(本地开发):**
|
||||
- 在端口 51121 上启动本地 HTTP 服务器
|
||||
- 等待来自 Google 的重定向
|
||||
- 从查询参数中提取授权码
|
||||
|
||||
**手动模式(远程/无头环境):**
|
||||
- 向用户显示授权 URL
|
||||
- 用户在浏览器中完成认证
|
||||
- 用户将完整的重定向 URL 粘贴回终端
|
||||
- 从粘贴的 URL 中解析授权码
|
||||
|
||||
#### 步骤 4:用授权码交换令牌
|
||||
```typescript
|
||||
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
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
#### 步骤 5:获取额外的用户数据
|
||||
|
||||
**用户邮箱:**
|
||||
```typescript
|
||||
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(API 调用必需):**
|
||||
```typescript
|
||||
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"; // 默认回退值
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## OAuth 实现细节
|
||||
|
||||
### 客户端凭据
|
||||
|
||||
**重要:** 这些凭据在源代码中以 base64 编码存储,用于与 pi-ai 同步:
|
||||
|
||||
```typescript
|
||||
const decode = (s: string) => Buffer.from(s, "base64").toString();
|
||||
|
||||
const CLIENT_ID = decode(
|
||||
"MTA3MTAwNjA2MDU5MS10bWhzc2luMmgyMWxjcmUyMzV2dG9sb2poNGc0MDNlcC5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbQ=="
|
||||
);
|
||||
const CLIENT_SECRET = decode("R09DU1BYLUs1OEZXUjQ4NkxkTEoxbUxCOHNYQzR6NnFEQWY=");
|
||||
```
|
||||
|
||||
### OAuth 流程模式
|
||||
|
||||
1. **自动流程**(有浏览器的本地机器):
|
||||
- 自动打开浏览器
|
||||
- 本地回调服务器捕获重定向
|
||||
- 初始认证后无需用户交互
|
||||
|
||||
2. **手动流程**(远程/无头/WSL2 环境):
|
||||
- 显示 URL 供手动复制粘贴
|
||||
- 用户在外部浏览器中完成认证
|
||||
- 用户将完整的重定向 URL 粘贴回来
|
||||
|
||||
```typescript
|
||||
function shouldUseManualOAuthFlow(isRemote: boolean): boolean {
|
||||
return isRemote || isWSL2Sync();
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 令牌管理
|
||||
|
||||
### 认证配置文件结构
|
||||
|
||||
```typescript
|
||||
type OAuthCredential = {
|
||||
type: "oauth";
|
||||
provider: "google-antigravity";
|
||||
access: string; // 访问令牌
|
||||
refresh: string; // 刷新令牌
|
||||
expires: number; // 过期时间戳(毫秒,自 epoch 起)
|
||||
email?: string; // 用户邮箱
|
||||
projectId?: string; // Google Cloud 项目 ID
|
||||
};
|
||||
```
|
||||
|
||||
### 令牌刷新
|
||||
|
||||
凭据包含一个刷新令牌,可在当前访问令牌过期时用于获取新的访问令牌。过期时间设置了 5 分钟的缓冲区以防止竞态条件。
|
||||
|
||||
---
|
||||
|
||||
## 模型列表获取
|
||||
|
||||
### 获取可用模型
|
||||
|
||||
```typescript
|
||||
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();
|
||||
|
||||
// 返回带有配额信息的模型
|
||||
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,
|
||||
},
|
||||
}));
|
||||
}
|
||||
```
|
||||
|
||||
### 响应格式
|
||||
|
||||
```typescript
|
||||
type FetchAvailableModelsResponse = {
|
||||
models?: Record<string, {
|
||||
displayName?: string;
|
||||
quotaInfo?: {
|
||||
remainingFraction?: number | string;
|
||||
resetTime?: string; // ISO 8601 时间戳
|
||||
isExhausted?: boolean;
|
||||
};
|
||||
}>;
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 用量追踪
|
||||
|
||||
### 获取用量数据
|
||||
|
||||
```typescript
|
||||
export async function fetchAntigravityUsage(
|
||||
token: string,
|
||||
timeoutMs: number
|
||||
): Promise<ProviderUsageSnapshot> {
|
||||
// 1. 获取额度和计划信息
|
||||
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",
|
||||
},
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
// 提取额度信息
|
||||
const { availablePromptCredits, planInfo, currentTier } = data;
|
||||
|
||||
// 2. 获取模型配额
|
||||
const modelsRes = await fetch(
|
||||
`${BASE_URL}/v1internal:fetchAvailableModels`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
body: JSON.stringify({ project: projectId }),
|
||||
}
|
||||
);
|
||||
|
||||
// 构建用量窗口
|
||||
return {
|
||||
provider: "google-antigravity",
|
||||
displayName: "Google Antigravity",
|
||||
windows: [
|
||||
{ label: "Credits", usedPercent: calculateUsedPercent(available, monthly) },
|
||||
// 各模型配额...
|
||||
],
|
||||
plan: currentTier?.name || planType,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### 用量响应结构
|
||||
|
||||
```typescript
|
||||
type ProviderUsageSnapshot = {
|
||||
provider: "google-antigravity";
|
||||
displayName: string;
|
||||
windows: UsageWindow[];
|
||||
plan?: string;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
type UsageWindow = {
|
||||
label: string; // "Credits" 或模型 ID
|
||||
usedPercent: number; // 0-100
|
||||
resetAt?: number; // 配额重置的时间戳
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 提供商插件结构
|
||||
|
||||
### 插件定义
|
||||
|
||||
```typescript
|
||||
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) => {
|
||||
// OAuth 实现在此处
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
### ProviderAuthContext
|
||||
|
||||
```typescript
|
||||
type ProviderAuthContext = {
|
||||
config: PicoClawConfig;
|
||||
agentDir?: string;
|
||||
workspaceDir?: string;
|
||||
prompter: WizardPrompter; // UI 提示/通知
|
||||
runtime: RuntimeEnv; // 日志等
|
||||
isRemote: boolean; // 是否在远程运行
|
||||
openUrl: (url: string) => Promise<void>; // 浏览器打开器
|
||||
oauth: {
|
||||
createVpsAwareHandlers: Function;
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
### ProviderAuthResult
|
||||
|
||||
```typescript
|
||||
type ProviderAuthResult = {
|
||||
profiles: Array<{
|
||||
profileId: string;
|
||||
credential: AuthProfileCredential;
|
||||
}>;
|
||||
configPatch?: Partial<PicoClawConfig>;
|
||||
defaultModel?: string;
|
||||
notes?: string[];
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 集成要求
|
||||
|
||||
### 1. 所需环境/依赖
|
||||
|
||||
- Go ≥ 1.25
|
||||
- PicoClaw 代码库(`pkg/providers/` 和 `pkg/auth/`)
|
||||
- `crypto` 和 `net/http` 标准库包
|
||||
|
||||
### 2. API 调用所需的请求头
|
||||
|
||||
```typescript
|
||||
const REQUIRED_HEADERS = {
|
||||
"Authorization": `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": "antigravity", // 或 "google-api-nodejs-client/9.15.1"
|
||||
"X-Goog-Api-Client": "google-cloud-sdk vscode_cloudshelleditor/0.1",
|
||||
};
|
||||
|
||||
// 对于 loadCodeAssist 调用,还需包含:
|
||||
const CLIENT_METADATA = {
|
||||
ideType: "ANTIGRAVITY", // 或 "IDE_UNSPECIFIED"
|
||||
platform: "PLATFORM_UNSPECIFIED",
|
||||
pluginType: "GEMINI",
|
||||
};
|
||||
```
|
||||
|
||||
### 3. 模型 Schema 清理
|
||||
|
||||
Antigravity 使用兼容 Gemini 的模型,因此工具 schema 必须进行清理:
|
||||
|
||||
```typescript
|
||||
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",
|
||||
]);
|
||||
|
||||
// 发送前清理 schema
|
||||
function cleanToolSchemaForGemini(schema: Record<string, unknown>): unknown {
|
||||
// 移除不支持的关键字
|
||||
// 确保顶层有 type: "object"
|
||||
// 展平 anyOf/oneOf 联合类型
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 思维块处理(Claude 模型)
|
||||
|
||||
对于 Antigravity 的 Claude 模型,思维块需要特殊处理:
|
||||
|
||||
```typescript
|
||||
const ANTIGRAVITY_SIGNATURE_RE = /^[A-Za-z0-9+/]+={0,2}$/;
|
||||
|
||||
export function sanitizeAntigravityThinkingBlocks(
|
||||
messages: AgentMessage[]
|
||||
): AgentMessage[] {
|
||||
// 验证思维签名
|
||||
// 规范化签名字段
|
||||
// 丢弃未签名的思维块
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API 端点
|
||||
|
||||
### 认证端点
|
||||
|
||||
| 端点 | 方法 | 用途 |
|
||||
|------|------|------|
|
||||
| `https://accounts.google.com/o/oauth2/v2/auth` | GET | OAuth 授权 |
|
||||
| `https://oauth2.googleapis.com/token` | POST | 令牌交换 |
|
||||
| `https://www.googleapis.com/oauth2/v1/userinfo` | GET | 用户信息(邮箱) |
|
||||
|
||||
### Cloud Code Assist 端点
|
||||
|
||||
| 端点 | 方法 | 用途 |
|
||||
|------|------|------|
|
||||
| `https://cloudcode-pa.googleapis.com/v1internal:loadCodeAssist` | POST | 加载项目信息、额度、计划 |
|
||||
| `https://cloudcode-pa.googleapis.com/v1internal:fetchAvailableModels` | POST | 列出可用模型及配额 |
|
||||
| `https://cloudcode-pa.googleapis.com/v1internal:streamGenerateContent?alt=sse` | POST | 聊天流式端点 |
|
||||
|
||||
**API 请求格式(聊天):**
|
||||
`v1internal:streamGenerateContent` 端点期望一个包装标准 Gemini 请求的信封格式:
|
||||
|
||||
```json
|
||||
{
|
||||
"project": "your-project-id",
|
||||
"model": "model-id",
|
||||
"request": {
|
||||
"contents": [...],
|
||||
"systemInstruction": {...},
|
||||
"generationConfig": {...},
|
||||
"tools": [...]
|
||||
},
|
||||
"requestType": "agent",
|
||||
"userAgent": "antigravity",
|
||||
"requestId": "agent-timestamp-random"
|
||||
}
|
||||
```
|
||||
|
||||
**API 响应格式(SSE):**
|
||||
每条 SSE 消息(`data: {...}`)被包装在 `response` 字段中:
|
||||
|
||||
```json
|
||||
{
|
||||
"response": {
|
||||
"candidates": [...],
|
||||
"usageMetadata": {...},
|
||||
"modelVersion": "...",
|
||||
"responseId": "..."
|
||||
},
|
||||
"traceId": "...",
|
||||
"metadata": {}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 配置
|
||||
|
||||
### config.json 配置
|
||||
|
||||
```json
|
||||
{
|
||||
"model_list": [
|
||||
{
|
||||
"model_name": "gemini-flash",
|
||||
"model": "antigravity/gemini-3-flash",
|
||||
"auth_method": "oauth"
|
||||
}
|
||||
],
|
||||
"agents": {
|
||||
"defaults": {
|
||||
"model_name": "gemini-flash"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 认证配置文件存储
|
||||
|
||||
认证配置文件存储在 `~/.picoclaw/auth.json` 中:
|
||||
|
||||
```json
|
||||
{
|
||||
"credentials": {
|
||||
"google-antigravity": {
|
||||
"access_token": "ya29...",
|
||||
"refresh_token": "1//...",
|
||||
"expires_at": "2026-01-01T00:00:00Z",
|
||||
"provider": "google-antigravity",
|
||||
"auth_method": "oauth",
|
||||
"email": "user@example.com",
|
||||
"project_id": "my-project-id"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 在 PicoClaw 中创建新提供商
|
||||
|
||||
PicoClaw 提供商以 Go 包的形式实现,位于 `pkg/providers/` 下。要添加新提供商:
|
||||
|
||||
### 分步实现
|
||||
|
||||
#### 1. 创建提供商文件
|
||||
|
||||
在 `pkg/providers/` 中创建新的 Go 文件:
|
||||
|
||||
```
|
||||
pkg/providers/
|
||||
└── your_provider.go
|
||||
```
|
||||
|
||||
#### 2. 实现 Provider 接口
|
||||
|
||||
你的提供商必须实现 `pkg/providers/types.go` 中定义的 `Provider` 接口:
|
||||
|
||||
```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 {
|
||||
// 实现带流式传输的聊天补全
|
||||
}
|
||||
```
|
||||
|
||||
#### 3. 在工厂中注册
|
||||
|
||||
将你的提供商添加到 `pkg/providers/factory.go` 中的协议分支:
|
||||
|
||||
```go
|
||||
case "your-provider":
|
||||
return NewYourProvider(sel.apiKey, sel.apiBase, sel.proxy), nil
|
||||
```
|
||||
|
||||
#### 4. 添加默认配置(可选)
|
||||
|
||||
在 `pkg/config/defaults.go` 中添加默认条目:
|
||||
|
||||
```go
|
||||
{
|
||||
ModelName: "your-model",
|
||||
Model: "your-provider/model-name",
|
||||
APIKey: "",
|
||||
},
|
||||
```
|
||||
|
||||
#### 5. 添加认证支持(可选)
|
||||
|
||||
如果你的提供商需要 OAuth 或特殊认证,在 `cmd/picoclaw/internal/auth/helpers.go` 中添加分支:
|
||||
|
||||
```go
|
||||
case "your-provider":
|
||||
authLoginYourProvider()
|
||||
```
|
||||
|
||||
#### 6. 通过 `config.json` 配置
|
||||
|
||||
```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"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 测试你的实现
|
||||
|
||||
### CLI 命令
|
||||
|
||||
```bash
|
||||
# 使用提供商进行认证
|
||||
picoclaw auth login --provider your-provider
|
||||
|
||||
# 列出模型(用于 Antigravity)
|
||||
picoclaw auth models
|
||||
|
||||
# 启动网关
|
||||
picoclaw gateway
|
||||
|
||||
# 使用指定模型运行代理
|
||||
picoclaw agent -m "Hello" --model your-model
|
||||
```
|
||||
|
||||
### 测试用环境变量
|
||||
|
||||
```bash
|
||||
# 覆盖默认模型
|
||||
export PICOCLAW_AGENTS_DEFAULTS_MODEL=your-model
|
||||
|
||||
# 覆盖提供商设置
|
||||
export PICOCLAW_MODEL_LIST='[{"model_name":"your-model","model":"your-provider/model-name","api_key":"..."}]'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 参考资料
|
||||
|
||||
- **源文件:**
|
||||
- `pkg/providers/antigravity_provider.go` - Antigravity 提供商实现
|
||||
- `pkg/auth/oauth.go` - OAuth 流程实现
|
||||
- `pkg/auth/store.go` - 认证凭据存储(`~/.picoclaw/auth.json`)
|
||||
- `pkg/providers/factory.go` - 提供商工厂和协议路由
|
||||
- `pkg/providers/types.go` - 提供商接口定义
|
||||
- `cmd/picoclaw/internal/auth/helpers.go` - 认证 CLI 命令
|
||||
|
||||
- **文档:**
|
||||
- `docs/ANTIGRAVITY_USAGE.md` - Antigravity 使用指南
|
||||
- `docs/migration/model-list-migration.md` - 迁移指南
|
||||
|
||||
---
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **Google Cloud 项目:** Antigravity 要求在你的 Google Cloud 项目上启用 Gemini for Google Cloud
|
||||
2. **配额:** 使用 Google Cloud 项目配额(非独立计费)
|
||||
3. **模型访问:** 可用模型取决于你的 Google Cloud 项目配置
|
||||
4. **思维块:** 通过 Antigravity 使用的 Claude 模型需要对带签名的思维块进行特殊处理
|
||||
5. **Schema 清理:** 工具 schema 必须清理以移除不支持的 JSON Schema 关键字
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
## 常见错误处理
|
||||
|
||||
### 1. 速率限制(HTTP 429)
|
||||
|
||||
当项目/模型配额耗尽时,Antigravity 会返回 429 错误。错误响应通常在 `details` 字段中包含 `quotaResetDelay`。
|
||||
|
||||
**429 错误示例:**
|
||||
```json
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 空响应(受限模型)
|
||||
|
||||
某些模型可能出现在可用模型列表中,但返回空响应(200 OK 但 SSE 流为空)。这通常发生在当前项目没有权限使用的预览版或受限模型上。
|
||||
|
||||
**处理方式:** 将空响应视为错误,通知用户该模型可能对其项目受限或无效。
|
||||
|
||||
---
|
||||
|
||||
## 故障排除
|
||||
|
||||
### "Token expired"(令牌已过期)
|
||||
- 刷新 OAuth 令牌:`picoclaw auth login --provider antigravity`
|
||||
|
||||
### "Gemini for Google Cloud is not enabled"(Gemini for Google Cloud 未启用)
|
||||
- 在 Google Cloud Console 中启用该 API
|
||||
|
||||
### "Project not found"(项目未找到)
|
||||
- 确保你的 Google Cloud 项目已启用必要的 API
|
||||
- 检查认证过程中项目 ID 是否正确获取
|
||||
|
||||
### 模型未出现在列表中
|
||||
- 验证 OAuth 认证是否成功完成
|
||||
- 检查认证配置文件存储:`~/.picoclaw/auth.json`
|
||||
- 重新运行 `picoclaw auth login --provider antigravity`
|
||||
@@ -0,0 +1,159 @@
|
||||
> Retour au [README](../project/README.fr.md)
|
||||
|
||||
# Chiffrement des identifiants
|
||||
|
||||
PicoClaw prend en charge le chiffrement des valeurs `api_key` dans les entrées de configuration `model_list`.
|
||||
Les clés chiffrées sont stockées sous forme de chaînes `enc://<base64>` et déchiffrées automatiquement au démarrage.
|
||||
|
||||
---
|
||||
|
||||
## Démarrage rapide
|
||||
|
||||
**1. Définir votre phrase secrète**
|
||||
|
||||
```bash
|
||||
export PICOCLAW_KEY_PASSPHRASE="your-passphrase"
|
||||
```
|
||||
|
||||
**2. Chiffrer une clé API**
|
||||
|
||||
Exécutez `picoclaw onboard` — il vous demande votre phrase secrète et génère la clé SSH,
|
||||
puis re-chiffre automatiquement toutes les entrées `api_key` en clair dans votre configuration
|
||||
lors du prochain appel à `SaveConfig`. La valeur `enc://` résultante ressemblera à :
|
||||
|
||||
```
|
||||
enc://AAAA...base64...
|
||||
```
|
||||
|
||||
**3. Coller la sortie dans votre configuration**
|
||||
|
||||
```json
|
||||
{
|
||||
"model_list": [
|
||||
{
|
||||
"model_name": "gpt-4o",
|
||||
"model": "openai/gpt-4o",
|
||||
"api_key": "enc://AAAA...base64...",
|
||||
"api_base": "https://api.openai.com/v1"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Formats `api_key` pris en charge
|
||||
|
||||
| Format | Exemple | Comportement |
|
||||
|--------|---------|--------------|
|
||||
| Texte clair | `sk-abc123` | Utilisé tel quel |
|
||||
| Référence fichier | `file://openai.key` | Contenu lu depuis le même répertoire que le fichier de configuration |
|
||||
| Chiffré | `enc://<base64>` | Déchiffré au démarrage avec `PICOCLAW_KEY_PASSPHRASE` |
|
||||
| Vide | `""` | Transmis tel quel (utilisé avec `auth_method: oauth`) |
|
||||
|
||||
---
|
||||
|
||||
## Conception cryptographique
|
||||
|
||||
### Dérivation de clé
|
||||
|
||||
Le chiffrement utilise **HKDF-SHA256** avec une clé privée SSH comme second facteur.
|
||||
|
||||
```
|
||||
sshHash = SHA256(ssh_private_key_file_bytes)
|
||||
ikm = HMAC-SHA256(key=sshHash, message=passphrase)
|
||||
aes_key = HKDF-SHA256(ikm, salt, info="picoclaw-credential-v1", 32 bytes)
|
||||
```
|
||||
|
||||
### Chiffrement
|
||||
|
||||
```
|
||||
AES-256-GCM(key=aes_key, nonce=random[12], plaintext=api_key)
|
||||
```
|
||||
|
||||
### Format de transmission
|
||||
|
||||
```
|
||||
enc://<base64( salt[16] + nonce[12] + ciphertext )>
|
||||
```
|
||||
|
||||
| Champ | Taille | Description |
|
||||
|-------|--------|-------------|
|
||||
| `salt` | 16 octets | Aléatoire par chiffrement ; fourni à HKDF |
|
||||
| `nonce` | 12 octets | Aléatoire par chiffrement ; IV AES-GCM |
|
||||
| `ciphertext` | variable | Texte chiffré AES-256-GCM + tag d'authentification de 16 octets |
|
||||
|
||||
Le tag d'authentification GCM est automatiquement ajouté au texte chiffré. Toute altération provoque l'échec du déchiffrement avec une erreur plutôt que de retourner un texte clair corrompu.
|
||||
|
||||
### Performance
|
||||
|
||||
| Opération | Durée (ARM Cortex-A) |
|
||||
|-----------|----------------------|
|
||||
| Dérivation de clé (HKDF) | < 1 ms |
|
||||
| Déchiffrement AES-256-GCM | < 1 ms |
|
||||
| **Surcoût total au démarrage** | **< 2 ms par clé** |
|
||||
|
||||
---
|
||||
|
||||
## Sécurité à deux facteurs avec clé SSH
|
||||
|
||||
Lorsqu'une clé privée SSH est fournie, casser le chiffrement nécessite **les deux** :
|
||||
|
||||
1. La **phrase secrète** (`PICOCLAW_KEY_PASSPHRASE`)
|
||||
2. Le **fichier de clé privée SSH**
|
||||
|
||||
Cela signifie qu'un fichier de configuration divulgué seul ne suffit pas pour récupérer la clé API, même si la phrase secrète est faible. La clé SSH apporte 256 bits d'entropie (Ed25519) indépendamment de la force de la phrase secrète.
|
||||
|
||||
### Modèle de menace
|
||||
|
||||
| Ce que l'attaquant possède | Peut-il déchiffrer ? |
|
||||
|---------------------------|---------------------|
|
||||
| Fichier de configuration uniquement | Non — nécessite la phrase secrète + la clé SSH |
|
||||
| Clé SSH uniquement | Non — nécessite la phrase secrète |
|
||||
| Phrase secrète uniquement | Non — nécessite la clé SSH |
|
||||
| Fichier de configuration + clé SSH + phrase secrète | Oui — compromission totale |
|
||||
|
||||
---
|
||||
|
||||
## Variables d'environnement
|
||||
|
||||
| Variable | Requis | Description |
|
||||
|----------|--------|-------------|
|
||||
| `PICOCLAW_KEY_PASSPHRASE` | Oui (pour `enc://`) | Phrase secrète utilisée pour la dérivation de clé |
|
||||
| `PICOCLAW_SSH_KEY_PATH` | Non | Chemin vers la clé privée SSH. Si non défini, détection automatique depuis `~/.ssh/picoclaw_ed25519.key` |
|
||||
|
||||
### Détection automatique de la clé SSH
|
||||
|
||||
Si `PICOCLAW_SSH_KEY_PATH` n'est pas défini, PicoClaw recherche la clé dédiée :
|
||||
|
||||
```
|
||||
~/.ssh/picoclaw_ed25519.key
|
||||
```
|
||||
|
||||
Ce fichier dédié évite les conflits avec les clés SSH existantes de l'utilisateur.
|
||||
Exécutez `picoclaw onboard` pour le générer automatiquement.
|
||||
|
||||
`os.UserHomeDir()` est utilisé pour la résolution multiplateforme du répertoire personnel (lit `USERPROFILE` sous Windows, `HOME` sous Unix/macOS).
|
||||
|
||||
> **Remarque :** Un fichier de clé SSH est requis pour le chiffrement des identifiants. Si aucune clé n'est trouvée et que `PICOCLAW_SSH_KEY_PATH` n'est pas défini, le chiffrement/déchiffrement échouera. Exécutez `picoclaw onboard` pour générer la clé automatiquement.
|
||||
|
||||
---
|
||||
|
||||
## Migration
|
||||
|
||||
Étant donné que les seuls éléments secrets sont `PICOCLAW_KEY_PASSPHRASE` et le fichier de clé privée SSH, la migration est simple :
|
||||
|
||||
1. Copiez le fichier de configuration sur la nouvelle machine.
|
||||
2. Définissez `PICOCLAW_KEY_PASSPHRASE` avec la même valeur.
|
||||
3. Copiez le fichier de clé privée SSH au même chemin (ou définissez `PICOCLAW_SSH_KEY_PATH` vers son nouvel emplacement).
|
||||
|
||||
Aucun re-chiffrement n'est nécessaire.
|
||||
|
||||
---
|
||||
|
||||
## Considérations de sécurité
|
||||
|
||||
- **La phrase secrète et la clé SSH sont toutes deux requises.** La clé SSH agit comme un second facteur — sans elle, le chiffrement/déchiffrement échouera. Exécutez `picoclaw onboard` pour générer la clé si elle n'existe pas.
|
||||
- **La clé SSH est en lecture seule à l'exécution.** PicoClaw n'écrit ni ne modifie jamais le fichier de clé SSH.
|
||||
- **Les clés en texte clair restent prises en charge.** Les configurations existantes sans `enc://` ne sont pas affectées.
|
||||
- **Le format `enc://` est versionné** via le champ `info` de HKDF (`picoclaw-credential-v1`), permettant de futures mises à niveau d'algorithme sans casser les valeurs chiffrées existantes.
|
||||
@@ -0,0 +1,158 @@
|
||||
> [README](../project/README.ja.md) に戻る
|
||||
|
||||
# クレデンシャル暗号化
|
||||
|
||||
PicoClaw は `model_list` 設定エントリの `api_key` 値の暗号化をサポートしています。
|
||||
暗号化されたキーは `enc://<base64>` 文字列として保存され、起動時に自動的に復号されます。
|
||||
|
||||
---
|
||||
|
||||
## クイックスタート
|
||||
|
||||
**1. パスフレーズを設定する**
|
||||
|
||||
```bash
|
||||
export PICOCLAW_KEY_PASSPHRASE="your-passphrase"
|
||||
```
|
||||
|
||||
**2. API キーを暗号化する**
|
||||
|
||||
`picoclaw onboard` を実行します — パスフレーズの入力を求められ、SSH キーが生成されます。
|
||||
その後、次の `SaveConfig` 呼び出し時に、設定内のすべての平文 `api_key` エントリが自動的に再暗号化されます。生成される `enc://` 値は以下のようになります:
|
||||
|
||||
```
|
||||
enc://AAAA...base64...
|
||||
```
|
||||
|
||||
**3. 出力を設定に貼り付ける**
|
||||
|
||||
```json
|
||||
{
|
||||
"model_list": [
|
||||
{
|
||||
"model_name": "gpt-4o",
|
||||
"model": "openai/gpt-4o",
|
||||
"api_key": "enc://AAAA...base64...",
|
||||
"api_base": "https://api.openai.com/v1"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## サポートされる `api_key` 形式
|
||||
|
||||
| 形式 | 例 | 動作 |
|
||||
|------|---|------|
|
||||
| 平文 | `sk-abc123` | そのまま使用 |
|
||||
| ファイル参照 | `file://openai.key` | 設定ファイルと同じディレクトリから内容を読み取り |
|
||||
| 暗号化 | `enc://<base64>` | 起動時に `PICOCLAW_KEY_PASSPHRASE` を使用して復号 |
|
||||
| 空 | `""` | そのまま渡される(`auth_method: oauth` で使用) |
|
||||
|
||||
---
|
||||
|
||||
## 暗号設計
|
||||
|
||||
### 鍵導出
|
||||
|
||||
暗号化には **HKDF-SHA256** を使用し、SSH 秘密鍵を第二要素とします。
|
||||
|
||||
```
|
||||
sshHash = SHA256(ssh_private_key_file_bytes)
|
||||
ikm = HMAC-SHA256(key=sshHash, message=passphrase)
|
||||
aes_key = HKDF-SHA256(ikm, salt, info="picoclaw-credential-v1", 32 bytes)
|
||||
```
|
||||
|
||||
### 暗号化
|
||||
|
||||
```
|
||||
AES-256-GCM(key=aes_key, nonce=random[12], plaintext=api_key)
|
||||
```
|
||||
|
||||
### ワイヤーフォーマット
|
||||
|
||||
```
|
||||
enc://<base64( salt[16] + nonce[12] + ciphertext )>
|
||||
```
|
||||
|
||||
| フィールド | サイズ | 説明 |
|
||||
|-----------|--------|------|
|
||||
| `salt` | 16 バイト | 暗号化ごとにランダム生成;HKDF に入力 |
|
||||
| `nonce` | 12 バイト | 暗号化ごとにランダム生成;AES-GCM IV |
|
||||
| `ciphertext` | 可変 | AES-256-GCM 暗号文 + 16 バイト認証タグ |
|
||||
|
||||
GCM 認証タグは暗号文に自動的に付加されます。改ざんがあった場合、破損した平文を返すのではなく、エラーで復号が失敗します。
|
||||
|
||||
### パフォーマンス
|
||||
|
||||
| 操作 | 所要時間 (ARM Cortex-A) |
|
||||
|------|------------------------|
|
||||
| 鍵導出 (HKDF) | < 1 ms |
|
||||
| AES-256-GCM 復号 | < 1 ms |
|
||||
| **起動時の総オーバーヘッド** | **キーあたり < 2 ms** |
|
||||
|
||||
---
|
||||
|
||||
## SSH キーによる二要素セキュリティ
|
||||
|
||||
SSH 秘密鍵が提供されている場合、暗号を破るには**両方**が必要です:
|
||||
|
||||
1. **パスフレーズ** (`PICOCLAW_KEY_PASSPHRASE`)
|
||||
2. **SSH 秘密鍵ファイル**
|
||||
|
||||
これは、設定ファイルが漏洩しただけでは、パスフレーズが弱い場合でも API キーを復元できないことを意味します。SSH キーはパスフレーズの強度に関係なく、256 ビットのエントロピー(Ed25519)を提供します。
|
||||
|
||||
### 脅威モデル
|
||||
|
||||
| 攻撃者が持っているもの | 復号可能か? |
|
||||
|----------------------|-------------|
|
||||
| 設定ファイルのみ | いいえ — パスフレーズ + SSH キーが必要 |
|
||||
| SSH キーのみ | いいえ — パスフレーズが必要 |
|
||||
| パスフレーズのみ | いいえ — SSH キーが必要 |
|
||||
| 設定ファイル + SSH キー + パスフレーズ | はい — 完全な侵害 |
|
||||
|
||||
---
|
||||
|
||||
## 環境変数
|
||||
|
||||
| 変数 | 必須 | 説明 |
|
||||
|------|------|------|
|
||||
| `PICOCLAW_KEY_PASSPHRASE` | はい(`enc://` 使用時) | 鍵導出に使用するパスフレーズ |
|
||||
| `PICOCLAW_SSH_KEY_PATH` | いいえ | SSH 秘密鍵のパス。未設定の場合、`~/.ssh/picoclaw_ed25519.key` から自動検出 |
|
||||
|
||||
### SSH キーの自動検出
|
||||
|
||||
`PICOCLAW_SSH_KEY_PATH` が設定されていない場合、PicoClaw は専用キーを探します:
|
||||
|
||||
```
|
||||
~/.ssh/picoclaw_ed25519.key
|
||||
```
|
||||
|
||||
この専用ファイルにより、ユーザーの既存の SSH キーとの競合を回避します。
|
||||
`picoclaw onboard` を実行すると自動的に生成されます。
|
||||
|
||||
`os.UserHomeDir()` はクロスプラットフォームのホームディレクトリ解決に使用されます(Windows では `USERPROFILE`、Unix/macOS では `HOME` を読み取ります)。
|
||||
|
||||
> **注意:** SSH キーファイルはクレデンシャル暗号化に必須です。キーが見つからず `PICOCLAW_SSH_KEY_PATH` も設定されていない場合、暗号化/復号は失敗します。`picoclaw onboard` を実行してキーを自動生成してください。
|
||||
|
||||
---
|
||||
|
||||
## 移行
|
||||
|
||||
唯一の秘密情報は `PICOCLAW_KEY_PASSPHRASE` と SSH 秘密鍵ファイルであるため、移行は簡単です:
|
||||
|
||||
1. 設定ファイルを新しいマシンにコピーします。
|
||||
2. `PICOCLAW_KEY_PASSPHRASE` を同じ値に設定します。
|
||||
3. SSH 秘密鍵ファイルを同じパスにコピーします(または `PICOCLAW_SSH_KEY_PATH` を新しい場所に設定します)。
|
||||
|
||||
再暗号化は不要です。
|
||||
|
||||
---
|
||||
|
||||
## セキュリティに関する考慮事項
|
||||
|
||||
- **パスフレーズと SSH キーの両方が必須です。** SSH キーは第二要素として機能します — これがなければ暗号化/復号は失敗します。キーが存在しない場合は `picoclaw onboard` を実行して生成してください。
|
||||
- **SSH キーは実行時に読み取り専用です。** PicoClaw は SSH キーファイルへの書き込みや変更を行いません。
|
||||
- **平文キーは引き続きサポートされます。** `enc://` を使用しない既存の設定は影響を受けません。
|
||||
- **`enc://` 形式はバージョン管理されています。** HKDF `info` フィールド(`picoclaw-credential-v1`)により、既存の暗号化値を壊すことなく将来のアルゴリズムアップグレードが可能です。
|
||||
@@ -0,0 +1,159 @@
|
||||
# Credential Encryption
|
||||
|
||||
PicoClaw supports encrypting `api_key`/`api_keys` values in `model_list` configuration entries.
|
||||
Encrypted keys are stored as `enc://<base64>` strings and decrypted automatically at startup.
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
|
||||
**1. Set your passphrase**
|
||||
|
||||
```bash
|
||||
export PICOCLAW_KEY_PASSPHRASE="your-passphrase"
|
||||
```
|
||||
|
||||
**2. Encrypt an API key**
|
||||
|
||||
Run `picoclaw onboard` — it prompts for your passphrase and generates the SSH key,
|
||||
then automatically re-encrypts any plaintext `api_key` entries in your config on
|
||||
the next `SaveConfig` call. The resulting `enc://` value will look like:
|
||||
|
||||
```
|
||||
enc://AAAA...base64...
|
||||
```
|
||||
|
||||
**3. Paste the output into your config**
|
||||
|
||||
```json
|
||||
{
|
||||
"model_list": [
|
||||
{
|
||||
"model_name": "gpt-4o",
|
||||
"model": "openai/gpt-4o",
|
||||
// "api_key": "enc://AAAA...base64..." move to .security.yml
|
||||
"api_base": "https://api.openai.com/v1"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Supported `api_key` Formats
|
||||
|
||||
The same formats apply to both `api_key` (singular) and individual elements in the `api_keys` (array) field:
|
||||
|
||||
| Format | Example | Behaviour |
|
||||
|--------|---------|-----------|
|
||||
| Plaintext | `sk-abc123` | Used as-is |
|
||||
| File reference | `file://openai.key` | Content read from the same directory as the config file |
|
||||
| Encrypted | `enc://<base64>` | Decrypted at startup using `PICOCLAW_KEY_PASSPHRASE` |
|
||||
| Empty | `""` | Passed through unchanged (used with `auth_method: oauth`) |
|
||||
|
||||
---
|
||||
|
||||
## Cryptographic Design
|
||||
|
||||
### Key Derivation
|
||||
|
||||
Encryption uses **HKDF-SHA256** with an SSH private key as a second factor.
|
||||
|
||||
```
|
||||
sshHash = SHA256(ssh_private_key_file_bytes)
|
||||
ikm = HMAC-SHA256(key=sshHash, message=passphrase)
|
||||
aes_key = HKDF-SHA256(ikm, salt, info="picoclaw-credential-v1", 32 bytes)
|
||||
```
|
||||
|
||||
### Encryption
|
||||
|
||||
```
|
||||
AES-256-GCM(key=aes_key, nonce=random[12], plaintext=api_key)
|
||||
```
|
||||
|
||||
### Wire Format
|
||||
|
||||
```
|
||||
enc://<base64( salt[16] + nonce[12] + ciphertext )>
|
||||
```
|
||||
|
||||
| Field | Size | Description |
|
||||
|-------|------|-------------|
|
||||
| `salt` | 16 bytes | Random per encryption; fed into HKDF |
|
||||
| `nonce` | 12 bytes | Random per encryption; AES-GCM IV |
|
||||
| `ciphertext` | variable | AES-256-GCM ciphertext + 16-byte authentication tag |
|
||||
|
||||
The GCM authentication tag is appended to the ciphertext automatically. Any tampering causes decryption to fail with an error rather than returning corrupt plaintext.
|
||||
|
||||
### Performance
|
||||
|
||||
| Operation | Time (ARM Cortex-A) |
|
||||
|-----------|---------------------|
|
||||
| Key derivation (HKDF) | < 1 ms |
|
||||
| AES-256-GCM decrypt | < 1 ms |
|
||||
| **Total startup overhead** | **< 2 ms per key** |
|
||||
|
||||
---
|
||||
|
||||
## Two-Factor Security with SSH Key
|
||||
|
||||
When a SSH private key is provided, breaking the encryption requires **both**:
|
||||
|
||||
1. The **passphrase** (`PICOCLAW_KEY_PASSPHRASE`)
|
||||
2. The **SSH private key file**
|
||||
|
||||
This means a leaked config file alone is not sufficient to recover the API key, even if the passphrase is weak. The SSH key contributes 256 bits of entropy (Ed25519) regardless of passphrase strength.
|
||||
|
||||
### Threat Model
|
||||
|
||||
| Attacker Has | Can Decrypt? |
|
||||
|---|---|
|
||||
| Config file only | No — needs passphrase + SSH key |
|
||||
| SSH key only | No — needs passphrase |
|
||||
| Passphrase only | No — needs SSH key |
|
||||
| Config file + SSH key + passphrase | Yes — full compromise |
|
||||
|
||||
---
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Required | Description |
|
||||
|----------|----------|-------------|
|
||||
| `PICOCLAW_KEY_PASSPHRASE` | Yes (for `enc://`) | Passphrase used for key derivation |
|
||||
| `PICOCLAW_SSH_KEY_PATH` | No | Path to SSH private key. If not set, auto-detects from `~/.ssh/picoclaw_ed25519.key` |
|
||||
|
||||
### SSH Key Auto-Detection
|
||||
|
||||
If `PICOCLAW_SSH_KEY_PATH` is not set, PicoClaw looks for the picoclaw-specific key:
|
||||
|
||||
```
|
||||
~/.ssh/picoclaw_ed25519.key
|
||||
```
|
||||
|
||||
This dedicated file avoids conflicts with the user's existing SSH keys.
|
||||
Run `picoclaw onboard` to generate it automatically.
|
||||
|
||||
`os.UserHomeDir()` is used for cross-platform home directory resolution (reads `USERPROFILE` on Windows, `HOME` on Unix/macOS).
|
||||
|
||||
> **Note:** An SSH key file is required for credential encryption. If no key is found and `PICOCLAW_SSH_KEY_PATH` is not set, encryption/decryption will fail. Run `picoclaw onboard` to generate the key automatically.
|
||||
|
||||
---
|
||||
|
||||
## Migration
|
||||
|
||||
Because the only secret material is `PICOCLAW_KEY_PASSPHRASE` and the SSH private key file, migration is straightforward:
|
||||
|
||||
1. Copy the config file to the new machine.
|
||||
2. Set `PICOCLAW_KEY_PASSPHRASE` to the same value.
|
||||
3. Copy the SSH private key file to the same path (or set `PICOCLAW_SSH_KEY_PATH` to its new location).
|
||||
|
||||
No re-encryption is needed.
|
||||
|
||||
---
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- **Both passphrase and SSH key are required.** The SSH key acts as a second factor — without it, encryption/decryption will fail. Run `picoclaw onboard` to generate the key if it doesn't exist.
|
||||
- **The SSH key is read-only at runtime.** PicoClaw never writes to or modifies the SSH key file.
|
||||
- **Plaintext keys remain supported.** Existing configs without `enc://` are unaffected.
|
||||
- **The `enc://` format is versioned** via the HKDF `info` field (`picoclaw-credential-v1`), allowing future algorithm upgrades without breaking existing encrypted values.
|
||||
@@ -0,0 +1,159 @@
|
||||
> Voltar ao [README](../project/README.pt-br.md)
|
||||
|
||||
# Criptografia de Credenciais
|
||||
|
||||
O PicoClaw suporta a criptografia de valores `api_key` nas entradas de configuração `model_list`.
|
||||
As chaves criptografadas são armazenadas como strings `enc://<base64>` e descriptografadas automaticamente na inicialização.
|
||||
|
||||
---
|
||||
|
||||
## Início Rápido
|
||||
|
||||
**1. Defina sua frase secreta**
|
||||
|
||||
```bash
|
||||
export PICOCLAW_KEY_PASSPHRASE="your-passphrase"
|
||||
```
|
||||
|
||||
**2. Criptografe uma chave de API**
|
||||
|
||||
Execute `picoclaw onboard` — ele solicita sua frase secreta e gera a chave SSH,
|
||||
depois recriptografa automaticamente quaisquer entradas `api_key` em texto simples na sua configuração
|
||||
na próxima chamada `SaveConfig`. O valor `enc://` resultante será semelhante a:
|
||||
|
||||
```
|
||||
enc://AAAA...base64...
|
||||
```
|
||||
|
||||
**3. Cole a saída na sua configuração**
|
||||
|
||||
```json
|
||||
{
|
||||
"model_list": [
|
||||
{
|
||||
"model_name": "gpt-4o",
|
||||
"model": "openai/gpt-4o",
|
||||
"api_key": "enc://AAAA...base64...",
|
||||
"api_base": "https://api.openai.com/v1"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Formatos de `api_key` Suportados
|
||||
|
||||
| Formato | Exemplo | Comportamento |
|
||||
|---------|---------|---------------|
|
||||
| Texto simples | `sk-abc123` | Usado como está |
|
||||
| Referência de arquivo | `file://openai.key` | Conteúdo lido do mesmo diretório do arquivo de configuração |
|
||||
| Criptografado | `enc://<base64>` | Descriptografado na inicialização usando `PICOCLAW_KEY_PASSPHRASE` |
|
||||
| Vazio | `""` | Passado sem alteração (usado com `auth_method: oauth`) |
|
||||
|
||||
---
|
||||
|
||||
## Design Criptográfico
|
||||
|
||||
### Derivação de Chave
|
||||
|
||||
A criptografia utiliza **HKDF-SHA256** com uma chave privada SSH como segundo fator.
|
||||
|
||||
```
|
||||
sshHash = SHA256(ssh_private_key_file_bytes)
|
||||
ikm = HMAC-SHA256(key=sshHash, message=passphrase)
|
||||
aes_key = HKDF-SHA256(ikm, salt, info="picoclaw-credential-v1", 32 bytes)
|
||||
```
|
||||
|
||||
### Criptografia
|
||||
|
||||
```
|
||||
AES-256-GCM(key=aes_key, nonce=random[12], plaintext=api_key)
|
||||
```
|
||||
|
||||
### Formato de Transmissão
|
||||
|
||||
```
|
||||
enc://<base64( salt[16] + nonce[12] + ciphertext )>
|
||||
```
|
||||
|
||||
| Campo | Tamanho | Descrição |
|
||||
|-------|---------|-----------|
|
||||
| `salt` | 16 bytes | Aleatório por criptografia; alimentado no HKDF |
|
||||
| `nonce` | 12 bytes | Aleatório por criptografia; IV do AES-GCM |
|
||||
| `ciphertext` | variável | Texto cifrado AES-256-GCM + tag de autenticação de 16 bytes |
|
||||
|
||||
O tag de autenticação GCM é anexado automaticamente ao texto cifrado. Qualquer adulteração faz com que a descriptografia falhe com um erro em vez de retornar texto simples corrompido.
|
||||
|
||||
### Desempenho
|
||||
|
||||
| Operação | Tempo (ARM Cortex-A) |
|
||||
|----------|----------------------|
|
||||
| Derivação de chave (HKDF) | < 1 ms |
|
||||
| Descriptografia AES-256-GCM | < 1 ms |
|
||||
| **Sobrecarga total na inicialização** | **< 2 ms por chave** |
|
||||
|
||||
---
|
||||
|
||||
## Segurança de Dois Fatores com Chave SSH
|
||||
|
||||
Quando uma chave privada SSH é fornecida, quebrar a criptografia requer **ambos**:
|
||||
|
||||
1. A **frase secreta** (`PICOCLAW_KEY_PASSPHRASE`)
|
||||
2. O **arquivo de chave privada SSH**
|
||||
|
||||
Isso significa que um arquivo de configuração vazado sozinho não é suficiente para recuperar a chave de API, mesmo que a frase secreta seja fraca. A chave SSH contribui com 256 bits de entropia (Ed25519) independentemente da força da frase secreta.
|
||||
|
||||
### Modelo de Ameaça
|
||||
|
||||
| O que o atacante possui | Pode descriptografar? |
|
||||
|------------------------|----------------------|
|
||||
| Apenas o arquivo de configuração | Não — necessita da frase secreta + chave SSH |
|
||||
| Apenas a chave SSH | Não — necessita da frase secreta |
|
||||
| Apenas a frase secreta | Não — necessita da chave SSH |
|
||||
| Arquivo de configuração + chave SSH + frase secreta | Sim — comprometimento total |
|
||||
|
||||
---
|
||||
|
||||
## Variáveis de Ambiente
|
||||
|
||||
| Variável | Obrigatório | Descrição |
|
||||
|----------|-------------|-----------|
|
||||
| `PICOCLAW_KEY_PASSPHRASE` | Sim (para `enc://`) | Frase secreta usada para derivação de chave |
|
||||
| `PICOCLAW_SSH_KEY_PATH` | Não | Caminho para a chave privada SSH. Se não definido, detecta automaticamente em `~/.ssh/picoclaw_ed25519.key` |
|
||||
|
||||
### Detecção Automática da Chave SSH
|
||||
|
||||
Se `PICOCLAW_SSH_KEY_PATH` não estiver definido, o PicoClaw procura a chave dedicada:
|
||||
|
||||
```
|
||||
~/.ssh/picoclaw_ed25519.key
|
||||
```
|
||||
|
||||
Este arquivo dedicado evita conflitos com as chaves SSH existentes do usuário.
|
||||
Execute `picoclaw onboard` para gerá-lo automaticamente.
|
||||
|
||||
`os.UserHomeDir()` é usado para resolução multiplataforma do diretório home (lê `USERPROFILE` no Windows, `HOME` no Unix/macOS).
|
||||
|
||||
> **Nota:** Um arquivo de chave SSH é obrigatório para a criptografia de credenciais. Se nenhuma chave for encontrada e `PICOCLAW_SSH_KEY_PATH` não estiver definido, a criptografia/descriptografia falhará. Execute `picoclaw onboard` para gerar a chave automaticamente.
|
||||
|
||||
---
|
||||
|
||||
## Migração
|
||||
|
||||
Como os únicos materiais secretos são `PICOCLAW_KEY_PASSPHRASE` e o arquivo de chave privada SSH, a migração é simples:
|
||||
|
||||
1. Copie o arquivo de configuração para a nova máquina.
|
||||
2. Defina `PICOCLAW_KEY_PASSPHRASE` com o mesmo valor.
|
||||
3. Copie o arquivo de chave privada SSH para o mesmo caminho (ou defina `PICOCLAW_SSH_KEY_PATH` para sua nova localização).
|
||||
|
||||
Nenhuma recriptografia é necessária.
|
||||
|
||||
---
|
||||
|
||||
## Considerações de Segurança
|
||||
|
||||
- **Tanto a frase secreta quanto a chave SSH são obrigatórias.** A chave SSH atua como um segundo fator — sem ela, a criptografia/descriptografia falhará. Execute `picoclaw onboard` para gerar a chave se ela não existir.
|
||||
- **A chave SSH é somente leitura em tempo de execução.** O PicoClaw nunca escreve ou modifica o arquivo de chave SSH.
|
||||
- **Chaves em texto simples continuam sendo suportadas.** Configurações existentes sem `enc://` não são afetadas.
|
||||
- **O formato `enc://` é versionado** através do campo `info` do HKDF (`picoclaw-credential-v1`), permitindo futuras atualizações de algoritmo sem quebrar valores criptografados existentes.
|
||||
@@ -0,0 +1,159 @@
|
||||
> Quay lại [README](../project/README.vi.md)
|
||||
|
||||
# Mã hóa Thông tin Xác thực
|
||||
|
||||
PicoClaw hỗ trợ mã hóa các giá trị `api_key` trong các mục cấu hình `model_list`.
|
||||
Các khóa đã mã hóa được lưu trữ dưới dạng chuỗi `enc://<base64>` và được giải mã tự động khi khởi động.
|
||||
|
||||
---
|
||||
|
||||
## Bắt đầu Nhanh
|
||||
|
||||
**1. Đặt cụm mật khẩu**
|
||||
|
||||
```bash
|
||||
export PICOCLAW_KEY_PASSPHRASE="your-passphrase"
|
||||
```
|
||||
|
||||
**2. Mã hóa khóa API**
|
||||
|
||||
Chạy `picoclaw onboard` — nó yêu cầu nhập cụm mật khẩu và tạo khóa SSH,
|
||||
sau đó tự động mã hóa lại tất cả các mục `api_key` dạng văn bản thuần trong cấu hình
|
||||
ở lần gọi `SaveConfig` tiếp theo. Giá trị `enc://` kết quả sẽ có dạng:
|
||||
|
||||
```
|
||||
enc://AAAA...base64...
|
||||
```
|
||||
|
||||
**3. Dán kết quả vào cấu hình**
|
||||
|
||||
```json
|
||||
{
|
||||
"model_list": [
|
||||
{
|
||||
"model_name": "gpt-4o",
|
||||
"model": "openai/gpt-4o",
|
||||
"api_key": "enc://AAAA...base64...",
|
||||
"api_base": "https://api.openai.com/v1"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Các Định dạng `api_key` được Hỗ trợ
|
||||
|
||||
| Định dạng | Ví dụ | Hành vi |
|
||||
|-----------|-------|---------|
|
||||
| Văn bản thuần | `sk-abc123` | Sử dụng nguyên trạng |
|
||||
| Tham chiếu tệp | `file://openai.key` | Nội dung được đọc từ cùng thư mục với tệp cấu hình |
|
||||
| Đã mã hóa | `enc://<base64>` | Giải mã khi khởi động bằng `PICOCLAW_KEY_PASSPHRASE` |
|
||||
| Trống | `""` | Truyền qua không thay đổi (dùng với `auth_method: oauth`) |
|
||||
|
||||
---
|
||||
|
||||
## Thiết kế Mật mã
|
||||
|
||||
### Dẫn xuất Khóa
|
||||
|
||||
Mã hóa sử dụng **HKDF-SHA256** với khóa riêng SSH làm yếu tố thứ hai.
|
||||
|
||||
```
|
||||
sshHash = SHA256(ssh_private_key_file_bytes)
|
||||
ikm = HMAC-SHA256(key=sshHash, message=passphrase)
|
||||
aes_key = HKDF-SHA256(ikm, salt, info="picoclaw-credential-v1", 32 bytes)
|
||||
```
|
||||
|
||||
### Mã hóa
|
||||
|
||||
```
|
||||
AES-256-GCM(key=aes_key, nonce=random[12], plaintext=api_key)
|
||||
```
|
||||
|
||||
### Định dạng Truyền tải
|
||||
|
||||
```
|
||||
enc://<base64( salt[16] + nonce[12] + ciphertext )>
|
||||
```
|
||||
|
||||
| Trường | Kích thước | Mô tả |
|
||||
|--------|-----------|-------|
|
||||
| `salt` | 16 byte | Ngẫu nhiên mỗi lần mã hóa; đưa vào HKDF |
|
||||
| `nonce` | 12 byte | Ngẫu nhiên mỗi lần mã hóa; IV của AES-GCM |
|
||||
| `ciphertext` | thay đổi | Bản mã AES-256-GCM + thẻ xác thực 16 byte |
|
||||
|
||||
Thẻ xác thực GCM được tự động nối vào bản mã. Bất kỳ sự giả mạo nào đều khiến giải mã thất bại với lỗi thay vì trả về văn bản thuần bị hỏng.
|
||||
|
||||
### Hiệu suất
|
||||
|
||||
| Thao tác | Thời gian (ARM Cortex-A) |
|
||||
|----------|--------------------------|
|
||||
| Dẫn xuất khóa (HKDF) | < 1 ms |
|
||||
| Giải mã AES-256-GCM | < 1 ms |
|
||||
| **Tổng chi phí khởi động** | **< 2 ms mỗi khóa** |
|
||||
|
||||
---
|
||||
|
||||
## Bảo mật Hai Yếu tố với Khóa SSH
|
||||
|
||||
Khi khóa riêng SSH được cung cấp, việc phá vỡ mã hóa yêu cầu **cả hai**:
|
||||
|
||||
1. **Cụm mật khẩu** (`PICOCLAW_KEY_PASSPHRASE`)
|
||||
2. **Tệp khóa riêng SSH**
|
||||
|
||||
Điều này có nghĩa là chỉ rò rỉ tệp cấu hình không đủ để khôi phục khóa API, ngay cả khi cụm mật khẩu yếu. Khóa SSH đóng góp 256 bit entropy (Ed25519) bất kể độ mạnh của cụm mật khẩu.
|
||||
|
||||
### Mô hình Mối đe dọa
|
||||
|
||||
| Kẻ tấn công có | Có thể giải mã? |
|
||||
|----------------|-----------------|
|
||||
| Chỉ tệp cấu hình | Không — cần cụm mật khẩu + khóa SSH |
|
||||
| Chỉ khóa SSH | Không — cần cụm mật khẩu |
|
||||
| Chỉ cụm mật khẩu | Không — cần khóa SSH |
|
||||
| Tệp cấu hình + khóa SSH + cụm mật khẩu | Có — xâm phạm hoàn toàn |
|
||||
|
||||
---
|
||||
|
||||
## Biến Môi trường
|
||||
|
||||
| Biến | Bắt buộc | Mô tả |
|
||||
|------|----------|-------|
|
||||
| `PICOCLAW_KEY_PASSPHRASE` | Có (cho `enc://`) | Cụm mật khẩu dùng để dẫn xuất khóa |
|
||||
| `PICOCLAW_SSH_KEY_PATH` | Không | Đường dẫn đến khóa riêng SSH. Nếu không đặt, tự động phát hiện từ `~/.ssh/picoclaw_ed25519.key` |
|
||||
|
||||
### Tự động Phát hiện Khóa SSH
|
||||
|
||||
Nếu `PICOCLAW_SSH_KEY_PATH` không được đặt, PicoClaw tìm khóa chuyên dụng:
|
||||
|
||||
```
|
||||
~/.ssh/picoclaw_ed25519.key
|
||||
```
|
||||
|
||||
Tệp chuyên dụng này tránh xung đột với các khóa SSH hiện có của người dùng.
|
||||
Chạy `picoclaw onboard` để tạo tự động.
|
||||
|
||||
`os.UserHomeDir()` được sử dụng để phân giải thư mục home đa nền tảng (đọc `USERPROFILE` trên Windows, `HOME` trên Unix/macOS).
|
||||
|
||||
> **Lưu ý:** Tệp khóa SSH là bắt buộc cho mã hóa thông tin xác thực. Nếu không tìm thấy khóa và `PICOCLAW_SSH_KEY_PATH` không được đặt, mã hóa/giải mã sẽ thất bại. Chạy `picoclaw onboard` để tạo khóa tự động.
|
||||
|
||||
---
|
||||
|
||||
## Di chuyển
|
||||
|
||||
Vì tài liệu bí mật duy nhất là `PICOCLAW_KEY_PASSPHRASE` và tệp khóa riêng SSH, việc di chuyển rất đơn giản:
|
||||
|
||||
1. Sao chép tệp cấu hình sang máy mới.
|
||||
2. Đặt `PICOCLAW_KEY_PASSPHRASE` với cùng giá trị.
|
||||
3. Sao chép tệp khóa riêng SSH đến cùng đường dẫn (hoặc đặt `PICOCLAW_SSH_KEY_PATH` đến vị trí mới).
|
||||
|
||||
Không cần mã hóa lại.
|
||||
|
||||
---
|
||||
|
||||
## Lưu ý về Bảo mật
|
||||
|
||||
- **Cả cụm mật khẩu và khóa SSH đều bắt buộc.** Khóa SSH đóng vai trò yếu tố thứ hai — không có nó, mã hóa/giải mã sẽ thất bại. Chạy `picoclaw onboard` để tạo khóa nếu chưa tồn tại.
|
||||
- **Khóa SSH chỉ đọc khi chạy.** PicoClaw không bao giờ ghi hoặc sửa đổi tệp khóa SSH.
|
||||
- **Khóa văn bản thuần vẫn được hỗ trợ.** Các cấu hình hiện có không dùng `enc://` không bị ảnh hưởng.
|
||||
- **Định dạng `enc://` được quản lý phiên bản** thông qua trường `info` của HKDF (`picoclaw-credential-v1`), cho phép nâng cấp thuật toán trong tương lai mà không làm hỏng các giá trị đã mã hóa hiện có.
|
||||
@@ -0,0 +1,158 @@
|
||||
> 返回 [README](../project/README.zh.md)
|
||||
|
||||
# 凭据加密
|
||||
|
||||
PicoClaw 支持对 `model_list` 配置条目中的 `api_key` 值进行加密。
|
||||
加密后的密钥以 `enc://<base64>` 字符串形式存储,并在启动时自动解密。
|
||||
|
||||
---
|
||||
|
||||
## 快速开始
|
||||
|
||||
**1. 设置密码短语**
|
||||
|
||||
```bash
|
||||
export PICOCLAW_KEY_PASSPHRASE="your-passphrase"
|
||||
```
|
||||
|
||||
**2. 加密 API 密钥**
|
||||
|
||||
运行 `picoclaw onboard` — 它会提示你输入密码短语并生成 SSH 密钥,
|
||||
然后在下一次 `SaveConfig` 调用时自动重新加密配置中所有明文 `api_key` 条目。生成的 `enc://` 值如下所示:
|
||||
|
||||
```
|
||||
enc://AAAA...base64...
|
||||
```
|
||||
|
||||
**3. 将输出粘贴到你的配置中**
|
||||
|
||||
```json
|
||||
{
|
||||
"model_list": [
|
||||
{
|
||||
"model_name": "gpt-4o",
|
||||
"model": "openai/gpt-4o",
|
||||
"api_key": "enc://AAAA...base64...",
|
||||
"api_base": "https://api.openai.com/v1"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 支持的 `api_key` 格式
|
||||
|
||||
| 格式 | 示例 | 行为 |
|
||||
|------|------|------|
|
||||
| 明文 | `sk-abc123` | 直接使用 |
|
||||
| 文件引用 | `file://openai.key` | 从配置文件所在目录读取内容 |
|
||||
| 加密 | `enc://<base64>` | 启动时使用 `PICOCLAW_KEY_PASSPHRASE` 解密 |
|
||||
| 空值 | `""` | 原样传递(用于 `auth_method: oauth`) |
|
||||
|
||||
---
|
||||
|
||||
## 加密设计
|
||||
|
||||
### 密钥派生
|
||||
|
||||
加密使用 **HKDF-SHA256**,并以 SSH 私钥作为第二因子。
|
||||
|
||||
```
|
||||
sshHash = SHA256(ssh_private_key_file_bytes)
|
||||
ikm = HMAC-SHA256(key=sshHash, message=passphrase)
|
||||
aes_key = HKDF-SHA256(ikm, salt, info="picoclaw-credential-v1", 32 bytes)
|
||||
```
|
||||
|
||||
### 加密
|
||||
|
||||
```
|
||||
AES-256-GCM(key=aes_key, nonce=random[12], plaintext=api_key)
|
||||
```
|
||||
|
||||
### 传输格式
|
||||
|
||||
```
|
||||
enc://<base64( salt[16] + nonce[12] + ciphertext )>
|
||||
```
|
||||
|
||||
| 字段 | 大小 | 描述 |
|
||||
|------|------|------|
|
||||
| `salt` | 16 字节 | 每次加密随机生成;输入 HKDF |
|
||||
| `nonce` | 12 字节 | 每次加密随机生成;AES-GCM IV |
|
||||
| `ciphertext` | 可变 | AES-256-GCM 密文 + 16 字节认证标签 |
|
||||
|
||||
GCM 认证标签会自动附加到密文之后。任何篡改都会导致解密失败并报错,而不是返回损坏的明文。
|
||||
|
||||
### 性能
|
||||
|
||||
| 操作 | 耗时 (ARM Cortex-A) |
|
||||
|------|---------------------|
|
||||
| 密钥派生 (HKDF) | < 1 ms |
|
||||
| AES-256-GCM 解密 | < 1 ms |
|
||||
| **启动总开销** | **每个密钥 < 2 ms** |
|
||||
|
||||
---
|
||||
|
||||
## 使用 SSH 密钥的双因子安全
|
||||
|
||||
当提供 SSH 私钥时,破解加密需要**同时具备**:
|
||||
|
||||
1. **密码短语** (`PICOCLAW_KEY_PASSPHRASE`)
|
||||
2. **SSH 私钥文件**
|
||||
|
||||
这意味着仅泄露配置文件不足以恢复 API 密钥,即使密码短语较弱也是如此。SSH 密钥贡献 256 位熵(Ed25519),与密码短语强度无关。
|
||||
|
||||
### 威胁模型
|
||||
|
||||
| 攻击者拥有 | 能否解密? |
|
||||
|------------|-----------|
|
||||
| 仅配置文件 | 否 — 需要密码短语 + SSH 密钥 |
|
||||
| 仅 SSH 密钥 | 否 — 需要密码短语 |
|
||||
| 仅密码短语 | 否 — 需要 SSH 密钥 |
|
||||
| 配置文件 + SSH 密钥 + 密码短语 | 是 — 完全泄露 |
|
||||
|
||||
---
|
||||
|
||||
## 环境变量
|
||||
|
||||
| 变量 | 是否必需 | 描述 |
|
||||
|------|----------|------|
|
||||
| `PICOCLAW_KEY_PASSPHRASE` | 是(用于 `enc://`) | 用于密钥派生的密码短语 |
|
||||
| `PICOCLAW_SSH_KEY_PATH` | 否 | SSH 私钥路径。如未设置,自动从 `~/.ssh/picoclaw_ed25519.key` 检测 |
|
||||
|
||||
### SSH 密钥自动检测
|
||||
|
||||
如果未设置 `PICOCLAW_SSH_KEY_PATH`,PicoClaw 会查找专用密钥:
|
||||
|
||||
```
|
||||
~/.ssh/picoclaw_ed25519.key
|
||||
```
|
||||
|
||||
此专用文件避免与用户现有的 SSH 密钥冲突。
|
||||
运行 `picoclaw onboard` 可自动生成该密钥。
|
||||
|
||||
`os.UserHomeDir()` 用于跨平台主目录解析(在 Windows 上读取 `USERPROFILE`,在 Unix/macOS 上读取 `HOME`)。
|
||||
|
||||
> **注意:** SSH 密钥文件是凭据加密的必要条件。如果未找到密钥且未设置 `PICOCLAW_SSH_KEY_PATH`,加密/解密将失败。运行 `picoclaw onboard` 可自动生成密钥。
|
||||
|
||||
---
|
||||
|
||||
## 迁移
|
||||
|
||||
由于唯一的密钥材料是 `PICOCLAW_KEY_PASSPHRASE` 和 SSH 私钥文件,迁移非常简单:
|
||||
|
||||
1. 将配置文件复制到新机器。
|
||||
2. 将 `PICOCLAW_KEY_PASSPHRASE` 设置为相同的值。
|
||||
3. 将 SSH 私钥文件复制到相同路径(或将 `PICOCLAW_SSH_KEY_PATH` 设置为新位置)。
|
||||
|
||||
无需重新加密。
|
||||
|
||||
---
|
||||
|
||||
## 安全注意事项
|
||||
|
||||
- **密码短语和 SSH 密钥都是必需的。** SSH 密钥作为第二因子 — 没有它,加密/解密将失败。如果密钥不存在,运行 `picoclaw onboard` 生成。
|
||||
- **SSH 密钥在运行时为只读。** PicoClaw 不会写入或修改 SSH 密钥文件。
|
||||
- **仍然支持明文密钥。** 不使用 `enc://` 的现有配置不受影响。
|
||||
- **`enc://` 格式通过版本控制**,通过 HKDF `info` 字段(`picoclaw-credential-v1`)实现,允许未来升级算法而不破坏现有加密值。
|
||||
@@ -0,0 +1,651 @@
|
||||
# Security Configuration
|
||||
|
||||
## Overview
|
||||
|
||||
PicoClaw supports separating sensitive data (API keys, tokens, secrets, passwords) from the main configuration by storing them in a `.security.yml` file. This improves security by:
|
||||
|
||||
1. **Separation of concerns**: Configuration settings and secrets are in separate files
|
||||
2. **Easier sharing**: The main config can be shared without exposing sensitive data
|
||||
3. **Better version control**: `.security.yml` should be added to `.gitignore`
|
||||
4. **Flexible deployment**: Different environments can use different security files
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
~/.picoclaw/
|
||||
├── config.json # Main configuration (safe to share)
|
||||
└── .security.yml # Security data (never share)
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
The security configuration works through **direct field mapping**, NOT through `ref:` string references. The system automatically loads values from `.security.yml` and applies them to the corresponding fields in `config.json`.
|
||||
|
||||
### Key Points:
|
||||
|
||||
- Values in `.security.yml` are automatically mapped to corresponding fields in the config
|
||||
- The mapping is based on field names and structure, not on reference strings
|
||||
- If a value exists in `.security.yml`, it **overrides** the value in `config.json`
|
||||
- You can omit sensitive fields from `config.json` entirely (recommended)
|
||||
|
||||
## Security Configuration Structure
|
||||
|
||||
### Complete Example: .security.yml
|
||||
|
||||
```yaml
|
||||
# Model API Keys
|
||||
# All models MUST use `api_keys` (plural) array format
|
||||
# Even a single key must be provided as an array with one element
|
||||
model_list:
|
||||
gpt-5.4:
|
||||
api_keys:
|
||||
- "sk-proj-your-actual-openai-key-1"
|
||||
- "sk-proj-your-actual-openai-key-2" # Optional: Multiple keys for failover
|
||||
claude-sonnet-4.6:
|
||||
api_keys:
|
||||
- "sk-ant-your-actual-anthropic-key" # Single key in array format
|
||||
|
||||
# Channel Tokens
|
||||
channels:
|
||||
telegram:
|
||||
token: "your-telegram-bot-token"
|
||||
feishu:
|
||||
app_secret: "your-feishu-app-secret"
|
||||
encrypt_key: "your-feishu-encrypt-key"
|
||||
verification_token: "your-feishu-verification-token"
|
||||
discord:
|
||||
token: "your-discord-bot-token"
|
||||
weixin:
|
||||
token: "your-weixin-token"
|
||||
qq:
|
||||
app_secret: "your-qq-app-secret"
|
||||
dingtalk:
|
||||
client_secret: "your-dingtalk-client-secret"
|
||||
slack:
|
||||
bot_token: "your-slack-bot-token"
|
||||
app_token: "your-slack-app-token"
|
||||
matrix:
|
||||
access_token: "your-matrix-access-token"
|
||||
line:
|
||||
channel_secret: "your-line-channel-secret"
|
||||
channel_access_token: "your-line-channel-access-token"
|
||||
onebot:
|
||||
access_token: "your-onebot-access-token"
|
||||
wecom:
|
||||
token: "your-wecom-token"
|
||||
encoding_aes_key: "your-wecom-encoding-aes-key"
|
||||
wecom_app:
|
||||
corp_secret: "your-wecom-app-corp-secret"
|
||||
token: "your-wecom-app-token"
|
||||
encoding_aes_key: "your-wecom-app-encoding-aes-key"
|
||||
wecom_aibot:
|
||||
secret: "your-wecom-aibot-secret"
|
||||
token: "your-wecom-aibot-token"
|
||||
encoding_aes_key: "your-wecom-aibot-encoding-aes-key"
|
||||
pico:
|
||||
token: "your-pico-token"
|
||||
irc:
|
||||
password: "your-irc-password"
|
||||
nickserv_password: "your-irc-nickserv-password"
|
||||
sasl_password: "your-irc-sasl-password"
|
||||
|
||||
# Web Tool API Keys
|
||||
web:
|
||||
brave:
|
||||
api_keys:
|
||||
- "BSAyour-brave-api-key-1"
|
||||
- "BSAyour-brave-api-key-2" # Optional: Multiple keys for failover
|
||||
tavily:
|
||||
api_keys:
|
||||
- "tvly-your-tavily-api-key" # Single key in array format
|
||||
perplexity:
|
||||
api_keys:
|
||||
- "pplx-your-perplexity-api-key" # Single key in array format
|
||||
glm_search:
|
||||
api_key: "your-glm-search-api-key" # GLMSearch uses single key format (not array)
|
||||
baidu_search:
|
||||
api_key: "your-baidu-search-api-key"
|
||||
|
||||
# Skills Registry Tokens
|
||||
skills:
|
||||
github:
|
||||
token: "your-github-token"
|
||||
clawhub:
|
||||
auth_token: "your-clawhub-auth-token"
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Step 1: Create .security.yml
|
||||
|
||||
Create or copy the security file:
|
||||
```bash
|
||||
cp security.example.yml ~/.picoclaw/.security.yml
|
||||
```
|
||||
|
||||
### Step 2: Fill in your actual values
|
||||
|
||||
Edit `~/.picoclaw/.security.yml` and replace placeholder values with your actual API keys and tokens.
|
||||
|
||||
### Step 3: Set proper permissions
|
||||
|
||||
```bash
|
||||
chmod 600 ~/.picoclaw/.security.yml
|
||||
```
|
||||
|
||||
### Step 4: Simplify config.json (Recommended)
|
||||
|
||||
You can now remove sensitive fields from `config.json` since they're loaded from `.security.yml`:
|
||||
|
||||
**Before:**
|
||||
```json
|
||||
{
|
||||
"model_list": [
|
||||
{
|
||||
"model_name": "gpt-5.4",
|
||||
"model": "openai/gpt-5.4",
|
||||
"api_base": "https://api.openai.com/v1",
|
||||
"api_key": "sk-your-actual-api-key-here"
|
||||
}
|
||||
],
|
||||
"channel_list": {
|
||||
"telegram": {
|
||||
"enabled": true,
|
||||
"type": "telegram",
|
||||
"token": "1234567890:ABCdefGHIjklMNOpqrsTUVwxyz"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**After:**
|
||||
```json
|
||||
{
|
||||
"model_list": [
|
||||
{
|
||||
"model_name": "gpt-5.4",
|
||||
"model": "openai/gpt-5.4",
|
||||
"api_base": "https://api.openai.com/v1"
|
||||
// api_key is now loaded from .security.yml
|
||||
}
|
||||
],
|
||||
"channel_list": {
|
||||
"telegram": {
|
||||
"enabled": true,
|
||||
"type": "telegram"
|
||||
// token is now loaded from .security.yml
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Step 5: Verify
|
||||
|
||||
Restart PicoClaw and verify it loads correctly:
|
||||
```bash
|
||||
picoclaw --version
|
||||
```
|
||||
|
||||
## Field Mapping Rules
|
||||
|
||||
### Models
|
||||
|
||||
**In .security.yml:**
|
||||
```yaml
|
||||
model_list:
|
||||
<model_name>:
|
||||
api_keys:
|
||||
- "key-1"
|
||||
- "key-2"
|
||||
```
|
||||
|
||||
**Mapping:**
|
||||
- Field `api_keys` (array) maps to the model's API keys
|
||||
- The `<model_name>` must match the `model_name` field in `config.json`
|
||||
- Supports indexed names (e.g., "gpt-5.4:0") - the system will also try the base name ("gpt-5.4")
|
||||
|
||||
### Channels
|
||||
|
||||
Each channel maps its fields directly:
|
||||
|
||||
**In .security.yml:**
|
||||
```yaml
|
||||
channels:
|
||||
telegram:
|
||||
token: "value"
|
||||
feishu:
|
||||
app_secret: "value"
|
||||
encrypt_key: "value"
|
||||
verification_token: "value"
|
||||
discord:
|
||||
token: "value"
|
||||
```
|
||||
|
||||
**Mapping:**
|
||||
- `channels.telegram.token` → `config.channels.telegram.token`
|
||||
- `channels.feishu.app_secret` → `config.channels.feishu.app_secret`
|
||||
- etc.
|
||||
|
||||
### Web Tools
|
||||
|
||||
**Brave, Tavily, Perplexity:**
|
||||
```yaml
|
||||
web:
|
||||
brave:
|
||||
api_keys:
|
||||
- "key-1"
|
||||
- "key-2"
|
||||
```
|
||||
- Use `api_keys` (plural) array format
|
||||
|
||||
**GLMSearch:**
|
||||
```yaml
|
||||
web:
|
||||
glm_search:
|
||||
api_key: "single-key-here"
|
||||
```
|
||||
- Use `api_key` (singular) single string format
|
||||
|
||||
**BaiduSearch:**
|
||||
```yaml
|
||||
web:
|
||||
baidu_search:
|
||||
api_key: "your-key"
|
||||
```
|
||||
- Use `api_key` (singular) single string format
|
||||
|
||||
### Skills
|
||||
|
||||
**In .security.yml:**
|
||||
```yaml
|
||||
skills:
|
||||
github:
|
||||
token: "value"
|
||||
clawhub:
|
||||
auth_token: "value"
|
||||
```
|
||||
|
||||
## API Key Formats
|
||||
|
||||
### Models - Single key
|
||||
|
||||
Use array format with one element:
|
||||
```yaml
|
||||
model_list:
|
||||
gpt-5.4:
|
||||
api_keys:
|
||||
- "sk-your-key"
|
||||
```
|
||||
|
||||
### Models - Multiple keys (Load Balancing & Failover)
|
||||
|
||||
Use array format with multiple elements:
|
||||
```yaml
|
||||
model_list:
|
||||
gpt-5.4:
|
||||
api_keys:
|
||||
- "sk-your-key-1"
|
||||
- "sk-your-key-2"
|
||||
- "sk-your-key-3"
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- **Load balancing**: Requests are distributed across multiple keys
|
||||
- **Failover**: Automatic switching to another key if one fails
|
||||
- **Rate limit management**: Distribute usage across multiple keys
|
||||
- **High availability**: Reduce downtime during API provider issues
|
||||
|
||||
### Web Tools (Brave/Tavily/Perplexity) - Single key
|
||||
|
||||
```yaml
|
||||
web:
|
||||
brave:
|
||||
api_keys:
|
||||
- "BSA-your-key"
|
||||
```
|
||||
|
||||
### Web Tools (Brave/Tavily/Perplexity) - Multiple keys
|
||||
|
||||
```yaml
|
||||
web:
|
||||
brave:
|
||||
api_keys:
|
||||
- "BSA-key-1"
|
||||
- "BSA-key-2"
|
||||
```
|
||||
|
||||
### Web Tool (GLMSearch/BaiduSearch) - Single key only
|
||||
|
||||
```yaml
|
||||
web:
|
||||
glm_search:
|
||||
api_key: "your-glm-key" # Single string (NOT array)
|
||||
baidu_search:
|
||||
api_key: "your-baidu-key" # Single string (NOT array)
|
||||
```
|
||||
|
||||
## Model Name Matching
|
||||
|
||||
The system supports intelligent model name matching in `.security.yml`:
|
||||
|
||||
### Example 1: Exact Match
|
||||
|
||||
**config.json:**
|
||||
```json
|
||||
{
|
||||
"model_name": "gpt-5.4:0"
|
||||
}
|
||||
```
|
||||
|
||||
**.security.yml (exact match with index):**
|
||||
```yaml
|
||||
model_list:
|
||||
gpt-5.4:0:
|
||||
api_keys: ["key-1"]
|
||||
```
|
||||
|
||||
### Example 2: Base Name Match
|
||||
|
||||
**config.json:**
|
||||
```json
|
||||
{
|
||||
"model_name": "gpt-5.4:0"
|
||||
}
|
||||
```
|
||||
|
||||
**.security.yml (base name without index):**
|
||||
```yaml
|
||||
model_list:
|
||||
gpt-5.4:
|
||||
api_keys: ["key-1", "key-2"]
|
||||
```
|
||||
|
||||
Both methods work. The base name match allows you to use simpler keys in `.security.yml` even when your config uses indexed model names for load balancing.
|
||||
|
||||
## Backward Compatibility
|
||||
|
||||
The system maintains full backward compatibility:
|
||||
|
||||
1. **Direct values**: You can still use direct values in `config.json` (not recommended for production)
|
||||
2. **Mixed usage**: You can have some fields in `.security.yml` and others in `config.json`
|
||||
3. **Optional security file**: If `.security.yml` doesn't exist, the system will only use values from `config.json`
|
||||
4. **Override behavior**: If a field exists in both files, `.security.yml` value takes precedence
|
||||
|
||||
## Environment Variables
|
||||
|
||||
You can override any security value using environment variables:
|
||||
|
||||
**For models:**
|
||||
```bash
|
||||
export PICOCLAW_CHANNELS_TELEGRAM_TOKEN="token-from-env"
|
||||
```
|
||||
|
||||
**For channels:**
|
||||
```bash
|
||||
export PICOCLAW_CHANNELS_TELEGRAM_TOKEN="token-from-env"
|
||||
export PICOCLAW_CHANNELS_FEISHU_APP_SECRET="secret-from-env"
|
||||
```
|
||||
|
||||
**For web tools:**
|
||||
```bash
|
||||
export PICOCLAW_TOOLS_WEB_BRAVE_API_KEY="key-from-env"
|
||||
export PICOCLAW_TOOLS_WEB_BAIDU_API_KEY="baidu-key-from-env"
|
||||
```
|
||||
|
||||
Environment variables have the highest priority and will override both `config.json` and `.security.yml` values.
|
||||
|
||||
The pattern is: `PICOCLAW_<SECTION>_<KEY>_<FIELD>` with underscores separating path segments and converted to uppercase.
|
||||
|
||||
## Security Best Practices
|
||||
|
||||
1. **Never commit `.security.yml`** to version control
|
||||
2. **Add to .gitignore**: Ensure `.security.yml` is in your `.gitignore` file
|
||||
3. **Set file permissions**: `chmod 600 ~/.picoclaw/.security.yml`
|
||||
4. **Use different keys** for different environments (dev, staging, production)
|
||||
5. **Rotate keys regularly** and update `.security.yml`
|
||||
6. **Backup securely**: Encrypt backups containing `.security.yml`. Note that config migrations automatically create date-stamped backups (e.g., `config.json.20260330.bak` and `.security.yml.20260330.bak`)
|
||||
7. **Review access**: Ensure only authorized users have read access to the file
|
||||
|
||||
## API
|
||||
|
||||
### loadSecurityConfig
|
||||
|
||||
```go
|
||||
func loadSecurityConfig(securityPath string) (*SecurityConfig, error)
|
||||
```
|
||||
|
||||
Loads the security configuration from `.security.yml`. Returns an empty `SecurityConfig` if the file doesn't exist.
|
||||
|
||||
### saveSecurityConfig
|
||||
|
||||
```go
|
||||
func saveSecurityConfig(securityPath string, sec *SecurityConfig) error
|
||||
```
|
||||
|
||||
Saves the security configuration to `.security.yml` with `0o600` permissions.
|
||||
|
||||
### applySecurityConfig
|
||||
|
||||
```go
|
||||
func applySecurityConfig(cfg *Config, sec *SecurityConfig) error
|
||||
```
|
||||
|
||||
Applies security configuration to the main config by copying values from `.security.yml` to the corresponding fields in the config.
|
||||
|
||||
### securityPath
|
||||
|
||||
```go
|
||||
func securityPath(configPath string) string
|
||||
```
|
||||
|
||||
Returns the path to `.security.yml` relative to the config file.
|
||||
|
||||
## Example: Complete Configuration
|
||||
|
||||
### config.json
|
||||
|
||||
```json
|
||||
{
|
||||
"version": 3,
|
||||
"agents": {
|
||||
"defaults": {
|
||||
"workspace": "~/picoclaw-workspace",
|
||||
"model_name": "gpt-5.4"
|
||||
}
|
||||
},
|
||||
"model_list": [
|
||||
{
|
||||
"model_name": "gpt-5.4",
|
||||
"model": "openai/gpt-5.4",
|
||||
"api_base": "https://api.openai.com/v1"
|
||||
},
|
||||
{
|
||||
"model_name": "claude-sonnet-4.6",
|
||||
"model": "anthropic/claude-sonnet-4.6",
|
||||
"api_base": "https://api.anthropic.com/v1"
|
||||
}
|
||||
],
|
||||
"channel_list": {
|
||||
"telegram": {
|
||||
"enabled": true,
|
||||
"type": "telegram"
|
||||
}
|
||||
},
|
||||
"tools": {
|
||||
"web": {
|
||||
"brave": {
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### .security.yml
|
||||
|
||||
```yaml
|
||||
model_list:
|
||||
gpt-5.4:
|
||||
api_keys:
|
||||
- "sk-proj-actual-openai-key-1"
|
||||
- "sk-proj-actual-openai-key-2"
|
||||
claude-sonnet-4.6:
|
||||
api_keys:
|
||||
- "sk-ant-actual-anthropic-key"
|
||||
|
||||
channels:
|
||||
telegram:
|
||||
token: "1234567890:ABCdefGHIjklMNOpqrsTUVwxyz"
|
||||
|
||||
web:
|
||||
brave:
|
||||
api_keys:
|
||||
- "BSAactualbravekey-1"
|
||||
- "BSAactualbravekey-2"
|
||||
tavily:
|
||||
api_keys:
|
||||
- "tvly-your-tavily-key"
|
||||
glm_search:
|
||||
api_key: "your-glm-key"
|
||||
baidu_search:
|
||||
api_key: "your-baidu-key"
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
Run the security configuration tests:
|
||||
|
||||
```bash
|
||||
go test ./pkg/config -run TestSecurityConfig
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Error: "failed to load security config"
|
||||
|
||||
- Verify `.security.yml` exists in the same directory as `config.json`
|
||||
- Check the YAML syntax is valid (use a YAML validator)
|
||||
- Ensure file permissions allow reading
|
||||
|
||||
### Error: "model security entry not found"
|
||||
|
||||
- Ensure the model name in `config.json` matches exactly in `.security.yml`
|
||||
- Check that the `model_list` section exists in `.security.yml`
|
||||
- For models with indexed names (e.g., "gpt-5.4:0"), ensure the exact name is used or check the base name without index
|
||||
- Verify the YAML structure is correct (proper indentation)
|
||||
|
||||
### Multiple API Keys Not Working
|
||||
|
||||
- Ensure you're using `api_keys` (plural) in `.security.yml` for models and web tools (except GLMSearch/BaiduSearch)
|
||||
- Check that the array format is correct in YAML (proper indentation with dashes)
|
||||
- Remember: Models, Brave, Tavily, Perplexity MUST use `api_keys` (array format)
|
||||
- GLMSearch and BaiduSearch MUST use `api_key` (single string format)
|
||||
|
||||
### Load Balancing/Failover Issues
|
||||
|
||||
- Verify all API keys in the `api_keys` array are valid
|
||||
- Check that all keys have the same rate limits and permissions
|
||||
- Monitor logs to see which keys are being used and failing
|
||||
- Ensure the `api_keys` array is properly formatted in YAML
|
||||
|
||||
### Keys Not Being Applied
|
||||
|
||||
- Check that `.security.yml` is in the same directory as `config.json`
|
||||
- Verify the file permissions allow reading (`chmod 600 ~/.picoclaw/.security.yml`)
|
||||
- Ensure the YAML structure matches the expected format
|
||||
- Check for typos in field names (case-sensitive)
|
||||
- Verify the model/channel names match exactly (case-sensitive)
|
||||
|
||||
## Migration Guide
|
||||
|
||||
### Step 1: Backup your config
|
||||
|
||||
The system automatically creates a date-stamped backup before saving a migrated config (e.g., `config.json.20260330.bak` and `.security.yml.20260330.bak`). If you prefer a manual backup:
|
||||
|
||||
```bash
|
||||
cp ~/.picoclaw/config.json ~/.picoclaw/config.json.backup
|
||||
```
|
||||
|
||||
### Step 2: Create .security.yml
|
||||
|
||||
```bash
|
||||
cp security.example.yml ~/.picoclaw/.security.yml
|
||||
```
|
||||
|
||||
### Step 3: Fill in your API keys
|
||||
|
||||
Edit `~/.picoclaw/.security.yml` and replace placeholder values with your actual keys.
|
||||
|
||||
### Step 4: Remove sensitive fields from config.json
|
||||
|
||||
Remove or comment out sensitive fields from `config.json`:
|
||||
- `api_key` fields from `model_list` entries
|
||||
- `token` fields from `channels`
|
||||
- `api_key` fields from `tools.web`
|
||||
- `token`/`auth_token` fields from `tools.skills`
|
||||
|
||||
### Step 5: Set proper permissions
|
||||
|
||||
```bash
|
||||
chmod 600 ~/.picoclaw/.security.yml
|
||||
```
|
||||
|
||||
### Step 6: Test
|
||||
|
||||
```bash
|
||||
picoclaw --version
|
||||
```
|
||||
|
||||
### Step 7: Verify functionality
|
||||
|
||||
Test your models and channels to ensure everything works correctly.
|
||||
|
||||
### Step 8: Clean up (optional)
|
||||
|
||||
If everything works, you can delete the backups:
|
||||
```bash
|
||||
rm ~/.picoclaw/config.json.backup
|
||||
# Also remove auto-generated date-stamped backups if desired:
|
||||
rm ~/.picoclaw/config.json.20*.bak ~/.picoclaw/.security.yml.20*.bak
|
||||
```
|
||||
|
||||
## Advanced: Encrypted API Keys
|
||||
|
||||
PicoClaw supports encrypting API keys in the security file for additional protection.
|
||||
|
||||
### Setup
|
||||
|
||||
1. Set a passphrase via environment variable:
|
||||
```bash
|
||||
export PICOCLAW_CREDENTIAL_PASSPHRASE="your-secure-passphrase"
|
||||
```
|
||||
|
||||
2. When saving config, API keys will be encrypted automatically:
|
||||
```go
|
||||
SaveConfig(path, config)
|
||||
```
|
||||
|
||||
### Encrypted Format
|
||||
|
||||
Encrypted keys are stored as:
|
||||
```yaml
|
||||
model_list:
|
||||
gpt-5.4:
|
||||
api_keys:
|
||||
- "enc://encrypted-base64-string"
|
||||
```
|
||||
|
||||
The system automatically decrypts keys at runtime when loading the configuration.
|
||||
|
||||
### Benefits
|
||||
|
||||
- Additional layer of security
|
||||
- Keys are encrypted at rest
|
||||
- Passphrase can be managed separately from the config file
|
||||
|
||||
### Important Notes
|
||||
|
||||
- Always backup your passphrase securely
|
||||
- If you lose the passphrase, you'll lose access to encrypted keys
|
||||
- Use a strong, unique passphrase
|
||||
- Never commit the passphrase to version control
|
||||
@@ -0,0 +1,107 @@
|
||||
# Sensitive Data Filtering
|
||||
|
||||
PicoClaw can filter sensitive values (API keys, tokens, secrets, passwords) from tool call results before they are sent to the LLM. This prevents the LLM from seeing its own credentials, which could otherwise leak through tool output or cause confusing behavior.
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
When the LLM uses a tool that returns its own credentials (e.g., a tool that echoes the API key being used), those values are automatically replaced with `[FILTERED]` in the message sent to the LLM.
|
||||
|
||||
Sensitive values are collected from [`.security.yml`](./credential_encryption.md) — the centralized storage for all sensitive configuration (API keys, tokens, secrets stored alongside `config.json`). This includes:
|
||||
|
||||
- Model API keys
|
||||
- Channel tokens (Telegram, Discord, Slack, Matrix, etc.)
|
||||
- Web tool API keys (Brave, Tavily, Perplexity, etc.)
|
||||
- Skills tokens (GitHub, ClawHub)
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
Sensitive data filtering is configured in the `tools` section of `config.json`:
|
||||
|
||||
| Config | Type | Default | Description |
|
||||
|--------|------|---------|-------------|
|
||||
| `filter_sensitive_data` | bool | `true` | Enable/disable filtering. When `false`, no filtering is performed. |
|
||||
| `filter_min_length` | int | `8` | Minimum content length to trigger filtering. Short content is skipped for performance. |
|
||||
|
||||
```json
|
||||
{
|
||||
"tools": {
|
||||
"filter_sensitive_data": true,
|
||||
"filter_min_length": 8
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Environment Variable
|
||||
|
||||
| Variable | Description |
|
||||
|----------|-------------|
|
||||
| `PICOCLAW_TOOLS_FILTER_SENSITIVE_DATA` | Set to `true` or `false` to override the config value |
|
||||
|
||||
---
|
||||
|
||||
## How It Works
|
||||
|
||||
1. **On startup**: All sensitive values are collected from `.security.yml` using reflection and compiled into a `strings.Replacer` (O(n+m) performance, computed once).
|
||||
|
||||
2. **Per tool result**: Before sending any tool result content to the LLM:
|
||||
- If `filter_sensitive_data` is `false`, content is passed through unchanged
|
||||
- If content length < `filter_min_length`, content is passed through unchanged (fast path)
|
||||
- Otherwise, all sensitive values are replaced with `[FILTERED]`
|
||||
|
||||
3. **Replacement**: Uses `strings.Replacer` for efficient O(n+m) string substitution, where n = content length and m = total sensitive value length.
|
||||
|
||||
---
|
||||
|
||||
## Example
|
||||
|
||||
Given the following `.security.yml`:
|
||||
|
||||
```yaml
|
||||
model_list:
|
||||
my-model:
|
||||
api_keys:
|
||||
- sk-secret-key-12345
|
||||
|
||||
channels:
|
||||
telegram:
|
||||
token: "123456:ABC-DEF"
|
||||
```
|
||||
|
||||
And a tool result containing:
|
||||
|
||||
```
|
||||
The model is using API key sk-secret-key-12345 and Telegram bot 123456:ABC-DEF
|
||||
```
|
||||
|
||||
The LLM will receive:
|
||||
|
||||
```
|
||||
The model is using API key [FILTERED] and Telegram bot [FILTERED]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Performance
|
||||
|
||||
- **Fast path**: Content shorter than `filter_min_length` (default 8) is returned unchanged without any string scanning
|
||||
- **Efficient replacement**: Uses `strings.Replacer` with O(n+m) complexity instead of regex
|
||||
- **Lazy initialization**: The replacement map is built once on first access via `sync.Once`
|
||||
|
||||
---
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- **Credential exposure prevention**: Without filtering, tools that echo credentials could cause the LLM to see its own API keys, potentially leading to confusion or credential leakage in logs
|
||||
- **Defense in depth**: Filtering complements (but does not replace) credential encryption — both features should be used together
|
||||
- **No false positives**: Only values explicitly stored in `.security.yml` are filtered; the LLM's general knowledge is unaffected
|
||||
|
||||
---
|
||||
|
||||
## Related
|
||||
|
||||
- [Credential Encryption](./credential_encryption.md) — encrypting API keys in config
|
||||
- [Tools Configuration](../reference/tools_configuration.md)
|
||||
@@ -0,0 +1,107 @@
|
||||
# 敏感数据过滤
|
||||
|
||||
PicoClaw 可以从工具调用结果中过滤敏感值(API 密钥、令牌、密码等),然后再发送给 LLM。这可以防止 LLM 看到自己的凭据,避免通过工具输出泄露或产生混淆行为。
|
||||
|
||||
---
|
||||
|
||||
## 概述
|
||||
|
||||
当 LLM 使用的工具返回其自身的凭据时(例如,一个回显正在使用的 API 密钥的工具),这些值会自动替换为 `[FILTERED]` 再发送给 LLM。
|
||||
|
||||
敏感值从 `.security.yml` 中收集 —— 这是所有敏感配置的集中存储,包括:
|
||||
|
||||
- 模型 API 密钥
|
||||
- 频道令牌(Telegram、Discord、Slack、Matrix 等)
|
||||
- Web 工具 API 密钥(Brave、Tavily、Perplexity 等)
|
||||
- 技能令牌(GitHub、ClawHub)
|
||||
|
||||
---
|
||||
|
||||
## 配置
|
||||
|
||||
敏感数据过滤在 `config.json` 的 `tools` 部分配置:
|
||||
|
||||
| 配置 | 类型 | 默认值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| `filter_sensitive_data` | bool | `true` | 启用/禁用过滤。为 `false` 时,不进行任何过滤。 |
|
||||
| `filter_min_length` | int | `8` | 触发过滤的最小内容长度。短内容会被跳过以提高性能。 |
|
||||
|
||||
```json
|
||||
{
|
||||
"tools": {
|
||||
"filter_sensitive_data": true,
|
||||
"filter_min_length": 8
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 环境变量
|
||||
|
||||
| 变量 | 说明 |
|
||||
|------|------|
|
||||
| `PICOCLAW_TOOLS_FILTER_SENSITIVE_DATA` | 设置为 `true` 或 `false` 以覆盖配置值 |
|
||||
|
||||
---
|
||||
|
||||
## 工作原理
|
||||
|
||||
1. **启动时**:使用反射从 `.security.yml` 中收集所有敏感值,并编译成 `strings.Replacer`(O(n+m) 性能,仅计算一次)。
|
||||
|
||||
2. **每个工具结果**:在将任何工具结果发送给 LLM 之前:
|
||||
- 如果 `filter_sensitive_data` 为 `false`,内容原样传递
|
||||
- 如果内容长度 < `filter_min_length`,内容原样传递(快速路径)
|
||||
- 否则,所有敏感值都会被替换为 `[FILTERED]`
|
||||
|
||||
3. **替换**:使用 `strings.Replacer` 进行高效的 O(n+m) 字符串替换,其中 n = 内容长度,m = 敏感值总长度。
|
||||
|
||||
---
|
||||
|
||||
## 示例
|
||||
|
||||
给定以下 `.security.yml`:
|
||||
|
||||
```yaml
|
||||
model_list:
|
||||
my-model:
|
||||
api_keys:
|
||||
- sk-secret-key-12345
|
||||
|
||||
channels:
|
||||
telegram:
|
||||
token: "123456:ABC-DEF"
|
||||
```
|
||||
|
||||
以及包含以下内容的工具结果:
|
||||
|
||||
```
|
||||
The model is using API key sk-secret-key-12345 and Telegram bot 123456:ABC-DEF
|
||||
```
|
||||
|
||||
LLM 将收到:
|
||||
|
||||
```
|
||||
The model is using API key [FILTERED] and Telegram bot [FILTERED]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 性能
|
||||
|
||||
- **快速路径**:短于 `filter_min_length`(默认 8)的内容会直接返回,不进行任何字符串扫描
|
||||
- **高效替换**:使用 `strings.Replacer`,复杂度为 O(n+m),而非正则表达式
|
||||
- **延迟初始化**:替换映射通过 `sync.Once` 在首次访问时构建一次
|
||||
|
||||
---
|
||||
|
||||
## 安全注意事项
|
||||
|
||||
- **凭据泄露防护**:如果没有过滤,返回凭据的工具可能导致 LLM 看到自己的 API 密钥,可能导致日志中泄露凭据或产生混淆
|
||||
- **纵深防御**:过滤是对凭据加密的补充(而非替代)—— 应同时使用这两个功能
|
||||
- **无误报**:只有明确存储在 `.security.yml` 中的值才会被过滤;LLM 的通用知识不受影响
|
||||
|
||||
---
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [凭据加密](./credential_encryption.zh.md) — 配置中 API 密钥的加密
|
||||
- [工具配置](../reference/tools_configuration.zh.md)
|
||||
Reference in New Issue
Block a user