Écrire un provider IA
Un provider IA dans arka-deck c’est :
- Un manifest (métadonnées statiques : label, modèles, type d’authentification)
- Une instance (configuration utilisateur : clé API, modèle, baseUrl, default) persistée dans
<arkaHome>/providers/providers.db - Un
ProviderTester(vérification connexion : check rapide ou live probe) - Un
ChatRuntime(stream de chat agentique) - Optionnellement un
LlmInvokerpour les workers 1-shot
Les clés API sont chiffrées AES-256-GCM côté serveur, jamais exposées à l’UI ni loggées.
Étape 1 — Créer la structure addon
Section intitulée « Étape 1 — Créer la structure addon »addons/<provider-name>/ manifest.json ← kind: provider, status: draft
src/ index.ts ← exports tester + runtime tester.ts ← impl ProviderTester tester.test.ts runtime.ts ← impl ChatRuntime specs/SPEC-../ plans/PLAN-../Étape 2 — Implémenter le tester
Section intitulée « Étape 2 — Implémenter le tester »// addons/<name>/src/tester.ts
import type { ProviderTester, ProviderTesterInput, TestResult } from '../../../core/ports/outbound/provider-tester.js';
export class MyProviderTester implements ProviderTester { async test(input: ProviderTesterInput): Promise<TestResult> { if (input.kind === 'check') return this.runCheck(input); return this.runLive(input); }
private async runCheck(input: ProviderTesterInput): Promise<TestResult> { if (!input.apiKey) return { ok: false, error: 'Clé API requise' }; return { ok: true }; }
private async runLive(input: ProviderTesterInput): Promise<TestResult> { try { const response = await fetch(/* endpoint provider */, { method: 'POST', headers: { 'Authorization': `Bearer ${input.apiKey}` }, body: JSON.stringify({ /* probe minimal */ }), signal: AbortSignal.timeout(30_000), }); if (!response.ok) return { ok: false, error: `${response.status}: ${response.statusText}` }; return { ok: true, latencyMs: /* ... */ }; } catch (err) { return { ok: false, error: err instanceof Error ? err.message : String(err) }; } }}Garanties à respecter :
- Pas de log de la clé API.
- Timeout strict (30s par défaut).
- Erreur tronquée à ~200 caractères.
- Pas de stack trace dans le
errorretourné.
Étape 3 — Câbler dans le composition root
Section intitulée « Étape 3 — Câbler dans le composition root »import { MyProviderTester } from '../../addons/<name>/src/tester.js';
testers.set('<manifest-id>', new MyProviderTester());Étape 4 — Ajouter le manifest
Section intitulée « Étape 4 — Ajouter le manifest »PROVIDER_MANIFESTS.push({ id: '<manifest-id>', label: 'Mon Provider', description: 'Description courte.', addonId: '<provider-name>', models: [ { id: 'model-fast', label: 'Modèle rapide' }, { id: 'model-pro', label: 'Modèle complet' }, ], authKind: 'apikey', // ou 'oauth' / 'none' apiKeyHint: 'https://docs.provider.com/api-keys', // URL aide création clé capabilities: { streaming: true, vision: false, functions: true },});Étape 5 — Implémenter le ChatRuntime
Section intitulée « Étape 5 — Implémenter le ChatRuntime »Pour permettre le chat (pas seulement le test connexion) :
// addons/<name>/src/runtime.ts
import type { ChatRuntime, StreamMessageInput, ChatStreamEvent } from '../../../core/ports/outbound/chat-runtime.js';
export class MyProviderRuntime implements ChatRuntime { async *streamMessage(input: StreamMessageInput): AsyncIterable<ChatStreamEvent> { // Mapper SSE provider → ChatStreamEvent (block-start / delta / block-end / done / error) }}Câbler dans composition/addons/chat-runtime-registry.ts.
Étape 6 — Tests
Section intitulée « Étape 6 — Tests »import { vi, describe, it, expect, beforeEach } from 'vitest';import { MyProviderTester } from './tester.js';
describe('MyProviderTester', () => { beforeEach(() => { vi.stubGlobal('fetch', vi.fn()); });
it('check refuse une clé vide', async () => { const tester = new MyProviderTester(); const result = await tester.test({ modelId: 'model-fast', kind: 'check' }); expect(result.ok).toBe(false); expect(result.error).toContain('Clé API requise'); });
it('live probe — succès', async () => { (fetch as ReturnType<typeof vi.fn>).mockResolvedValueOnce({ ok: true, status: 200, json: async () => ({ /* payload */ }), }); // ... });});Cas à couvrir : check OK, clé vide, 401, timeout, réponse vide, erreur réseau.
Sécurité
Section intitulée « Sécurité »| Garantie | Mécanisme |
|---|---|
| Clé jamais en clair sur disque | AES-256-GCM dans SQLite |
| Clé maître protégée | <arkaHome>/secrets/secrets.key, mode 600 |
| Clé jamais retournée à l’UI | DTO public expose apiKeyConfigured: boolean seulement |
| Pas de log clé | Tester reçoit la clé déchiffrée en mémoire éphémère |
| Transmission externe contrôlée | Seul le live test POST la clé au provider |
| URL runtime | Loopback + *.arkalabs.app HTTPS par défaut, host externe refusé (ADR 0006) |
Anti-patterns à éviter
Section intitulée « Anti-patterns à éviter »Stocker la clé en clair côté UI
Section intitulée « Stocker la clé en clair côté UI »// ❌ JAMAISlocalStorage.setItem('provider-key', apiKey);
// ✅ Toujours envoyer au serveur, qui chiffre + stockeawait providersApi.createInstance({ name, manifestId, modelId, apiKey });Logger une instance complète
Section intitulée « Logger une instance complète »// ❌ peut leaker la clélogger.info('instance créée', instance);
// ✅logger.info('instance créée', { id: instance.id, manifestId: instance.manifestId });Hardcoder un modèle dans le code
Section intitulée « Hardcoder un modèle dans le code »// ❌const model = 'gemini-2.5-flash';
// ✅const inst = await forProviderInstances.getById(instanceId);const model = inst.modelId; // user-configurable via l'UIRéférence rapide
Section intitulée « Référence rapide »| Action | Fichier |
|---|---|
| Domaine instance | core/domain/providers/provider-instance.ts |
| Cipher AES-GCM | adapters/outbound/secrets/fs-secret-cipher.ts |
| Use-cases CRUD | core/use-cases/providers/build-for-provider-instances.ts |
| Routes serveur | adapters/inbound/web/server/src/routes/providers.ts |
| Manifests catalogue | composition/addons/provider-manifests.ts |
| Tester registry | composition/addons/providers-tester-registry.ts |
| Runtime registry (chat) | composition/addons/chat-runtime-registry.ts |