Contrat addon — référence formelle
Ce document décrit l’interface formelle qu’un addon doit respecter pour s’intégrer dans arka-deck. Il complète le tutoriel écrire un addon avec les types exacts et les garanties contractuelles.
Source de vérité pour les types : core/ports/outbound/, core/domain/events/arka-event.ts.
manifest.json
Section intitulée « manifest.json »Chaque addon déclare un manifest.json à sa racine.
{ "name": "mon-addon", "version": "1.0.0", "kind": "capability", "description": "Description courte de l'addon.", "status": "active", "workers": ["nom-worker-1"], "dependencies": [], "entrypoint": "src/index.ts"}| Champ | Type | Valeurs | Obligatoire |
|---|---|---|---|
name | string | kebab-case | oui |
version | string | semver | oui |
kind | string | "capability" / "orchestration" / "feature" / "convention" | oui |
description | string | max ~200 chars | oui |
status | string | "active" / "draft" | oui |
workers | string[] | noms des workers rattachés | oui (vide si aucun) |
dependencies | string[] | noms d’autres addons requis | oui (vide si aucun) |
entrypoint | string | chemin relatif depuis la racine de l’addon | oui pour capability / orchestration |
label | string | label affichable | non |
runtime | string | "cli" / "web" / "shared" | non |
tier | string | "free" / "standalone" / "cortex" / "squad-business" | non |
exposes | object | métadonnées déclaratives | non |
Le schéma exécutable est composition/addons/manifest-schema.ts.
Les valeurs kind du manifest JSON sont alignées avec la catégorie runtime AddonManifest.category du registre TypeScript. Les providers LLM ne sont pas un kind addon : ils utilisent le schéma séparé ProviderManifestSchema.
Chargement first-party
Section intitulée « Chargement first-party »Au démarrage, la composition charge les manifests first-party via composition/addons/loader.ts et vérifie aussi les références addon ↔ worker. Les warnings sont journalisés, mais le chargement déclaratif ne remplace pas le câblage explicite des runtimes dans la composition root.
Chemin canonique pour ajouter un addon first-party (SPEC-05, décision B)
Section intitulée « Chemin canonique pour ajouter un addon first-party (SPEC-05, décision B) »Un addon first-party ne doit PAS exiger une édition directe de composition/core-container.ts pour être enregistré. Le chemin canonique est :
- Déclarer le module addon sous
addons/<name>/avec sonmanifest.json, une fonctionregister<Name>Addon(deps)(pourAddon) ou un runtime construit par la composition (pourRegisteredRuntimeAddon). - Ajouter le manifest dans
composition/addons/index.ts→FIRST_PARTY_RUNTIME_ADDON_MANIFESTS(ou, pour unAddonauto-enregistré, dans le tableauADDONS). - Câbler via
composition/addons/first-party-runtime.ts→registerFirstPartyRuntimeAddons(deps). C’est le point d’entrée unique appelé par le composition root ; ajouter un addon ne requiert qu’une extension du recorddepset de la liste de manifests ci-dessus. - Exposer des routes HTTP (optionnel) via
composition/addons/<name>-routes.ts, montées depuiscomposition/web-container.ts.
Le composition root doit conserver un seul site d’appel pour le câblage des addons (registerFirstPartyRuntimeAddons). Les appels directs register<Name>Addon(...) dans core-container.ts sont tracés comme dérogation SPEC-05 et listés dans ROADMAP.md sous « Chantiers de split tracés (SPEC-05) ».
Aujourd’hui le composition root appelle encore directement
registerCortexActionsAddonetregisterLocalMemoryAddoncar leurs runtimes dépendent de builders locaux au container qui n’ont pas encore été extraits. La cible est de les pousser dans le chemin canonique via des helpers factory dédiés souscomposition/addons/.
Limite publique actuelle
Section intitulée « Limite publique actuelle »Le registre d’addons est une brique interne et first-party. Il valide les manifests et fournit un contrat stable, mais arka-deck ne charge pas encore un marketplace tiers arbitraire en production.
Conséquence pour la documentation publique : parler de “contrat addon” ou “addons first-party extensibles”, pas d’un marketplace public déjà actif. Les ajouts runtime restent câblés explicitement dans la composition root.
Le registre accepte deux formes distinctes :
Addon: le registre appelleregister(deps)et construit le runtime.RegisteredRuntimeAddon: la composition root a déjà construit le runtime ; le registre le stocke tel quel et ne rappelle pasregister(deps).
Cette distinction évite de présenter les addons déjà câblés (cortex-actions, memory-local, mission-guardian) comme s’ils étaient construits par le registre.
Fonction register
Section intitulée « Fonction register »Le point d’entrée d’un addon est une fonction register<Name>Addon(deps) exportée depuis src/index.ts.
export interface MonAddonDeps { readonly clock: Clock; readonly filesystem: Filesystem; readonly eventBus: EventBus; readonly idGenerator?: IdGenerator; readonly resolveSession: (sessionId: string) => Promise<SessionContext | null>; readonly cortexBaseUrl?: string; // si l'addon fait des appels HTTP Cortex}
export interface MonAddonRuntime { readonly forMonAddon: ForMonAddon; readonly beforeTurnAugmenter: BeforeTurnAugmenter; readonly unsubscribe: () => void;}
export function registerMonAddon(deps: MonAddonDeps): MonAddonRuntime { // 1. Instancier les adapters // 2. Construire les use-cases // 3. S'abonner au bus // 4. Construire l'augmenter (si applicable) return { forMonAddon, beforeTurnAugmenter, unsubscribe };}Règle : unsubscribe() doit toujours être présent et idempotent. Il est appelé au shutdown.
Dépendances disponibles
Section intitulée « Dépendances disponibles »La composition (composition/core-container.ts) fournit ces dépendances à chaque addon :
| Dep | Type | Source | Usage |
|---|---|---|---|
clock | Clock | core/ports/outbound/clock.ts | Timestamp actuel (clock.now()) |
filesystem | Filesystem | core/ports/outbound/filesystem.ts | Lecture/écriture fichiers (allowlist) |
eventBus | EventBus | core/ports/outbound/event-bus.ts | S’abonner et publier des events |
idGenerator | IdGenerator | core/ports/outbound/id-generator.ts | Générer des IDs stables |
resolveSession | (sessionId) => Promise<Context|null> | Composition | Résoudre projectPath depuis sessionId |
cortexBaseUrl | string? | Env ARKA_DECK_CORTEX_URL | Override URL Cortex pour tests |
EventBus
Section intitulée « EventBus »export type Unsubscribe = () => void;
export interface EventBus { publish(event: ArkaEvent): Promise<void>; publishAsync(event: ArkaEvent): void; subscribe<T extends ArkaEventType>(type: T, handler: EventHandler<T>): Unsubscribe;}Garanties :
- Une erreur dans un handler n’impacte pas les autres (isolation via
allSettled) - L’ordre des handlers n’est pas garanti
unsubscribeest idempotent- Le bus n’est pas persistant — les events produits avant l’abonnement ne sont pas rejouables
Pour le guide détaillé, voir ../architecture/event-bus.
Catalogue des events (ArkaEvent)
Section intitulée « Catalogue des events (ArkaEvent) »Source de vérité : core/domain/events/arka-event.ts.
Workspace
Section intitulée « Workspace »| Type | Champs clés |
|---|---|
workspace.created | workspaceId, name, createdAt |
workspace.updated | workspaceId, name, description |
workspace.deleted | workspaceId |
| Type | Champs clés |
|---|---|
project.created | projectId, projectPath, workspaceId, name |
project.updated | projectId, name, workspaceId |
project.forgotten | projectId |
project.purged | projectId, projectPath, deletedPaths |
project.last-used | projectId, lastUsedAt |
project.detected-by-cwd | projectId, projectPath, cwd |
Provider instances
Section intitulée « Provider instances »| Type | Champs clés |
|---|---|
provider.instance.created | instanceId, manifestId, modelId, isDefault |
provider.instance.updated | instanceId, changes |
provider.instance.deleted | instanceId |
provider.instance.tested | instanceId, kind, ok, latencyMs, error? |
Catalogue / Installs
Section intitulée « Catalogue / Installs »| Type | Champs clés |
|---|---|
catalogue.refreshed | source, entriesCount |
agent.installed | projectPath, sourceId, version, localPath |
agent.removed | projectPath, sourceId |
agent.updated | projectPath, sourceId, fromVersion, toVersion |
agent.relinked | projectPath, sourceId |
agent.divergence-detected | projectPath, transitions, items |
Chat — session lifecycle
Section intitulée « Chat — session lifecycle »| Type | Champs clés |
|---|---|
chat.session.started | sessionId, projectId, projectPath?, agentId?, runtimeId, modelId, createdAt |
chat.session.resumed | sessionId, runtimeSessionId? |
chat.session.ended | sessionId, projectPath?, transcript, reason, endedAt |
chat.session.superseded | sessionId, supersededBySessionId, projectId, agentId, archivedAt |
chat.session.renamed | sessionId, name |
chat.session.deleted | sessionId |
chat.session.preparing.started | sessionId, projectId, agentId?, startedAt |
chat.session.preparing.ended | sessionId, runtimeSessionId?, durationMs, usage?, endedAt |
chat.session.preparing.failed | sessionId, error, durationMs, failedAt |
Chat — turn lifecycle
Section intitulée « Chat — turn lifecycle »| Type | Champs clés |
|---|---|
chat.turn.started | sessionId, userText, attachmentsCount, startedAt |
chat.turn.ended | sessionId, success: true, durationMs, usage? |
chat.turn.aborted | sessionId, reason, error? |
Chat — blocs
Section intitulée « Chat — blocs »| Type | Champs clés |
|---|---|
chat.block.started | sessionId, blockId, kind, tool? |
chat.block.ended | sessionId, blockId, kind, content? |
Chat — tools
Section intitulée « Chat — tools »| Type | Champs clés |
|---|---|
chat.tool.called | sessionId, toolUseId, tool, input |
chat.tool.result | sessionId, toolUseId, status |
chat.tool.approval-requested | sessionId, requestId, tool, input |
chat.tool.approval-decided | sessionId, requestId, decision, message? |
Chat — compaction
Section intitulée « Chat — compaction »| Type | Champs clés |
|---|---|
chat.compact.pre | sessionId, projectPath?, transcript |
chat.compact.post | sessionId |
Chat — slash commands
Section intitulée « Chat — slash commands »| Type | Champs clés |
|---|---|
chat.slash-command.detected | sessionId, commands |
chat.memorize | sessionId, projectPath?, text |
chat.recap-project | sessionId, projectPath |
chat.memorize-learning | sessionId, projectPath?, text |
Chat — attachments et runtime
Section intitulée « Chat — attachments et runtime »| Type | Champs clés |
|---|---|
chat.attachment.added | sessionId, kind, mediaType, sizeBytes |
chat.runtime.session-id-captured | sessionId, runtimeSessionId |
chat.runtime.failure | sessionId, projectPath?, error, transcript |
| Type | Champs clés |
|---|---|
memory.entry.captured | projectPath, entryId, kind, health, trigger |
memory.recall-pack.composed | projectPath, tokens, entriesCount |
memory.learning.proposed | projectPath, learningId |
memory.learning.validated | projectPath, learningId |
memory.degraded | projectPath, reason |
Settings, System, Addons, Squad
Section intitulée « Settings, System, Addons, Squad »| Type | Champs clés |
|---|---|
settings.preferences.changed | key, value |
settings.launch-options.changed | projectId, key, value |
system.boot.completed | version, addons |
system.online-state.changed | online |
system.shutdown.requested | reason |
addon.registered | addonId, kind, version |
worker.registered | workerId, addonParent, runtime |
squad.mission.event-emitted | missionId, projectPath, sequence, persistedAt, event |
orchestration.run.event-emitted | runId, projectPath, sequence, persistedAt, event |
BeforeTurnAugmenter
Section intitulée « BeforeTurnAugmenter »Port core/ports/outbound/before-turn-augmenter.ts. Implémentez ce port si votre addon doit injecter du contexte dans un tour de chat.
export interface BeforeTurnAugmenter { augmentPrompt(input: BeforeTurnAugmentInput): Promise<string | null>;}
export interface BeforeTurnAugmentInput { readonly sessionId: string; readonly turnKind?: 'silent_prepare' | 'visible_user' | 'resume'; readonly visibleTurnIndex?: number;}Comportement attendu :
- Retourner
nullsi pas d’augmentation pour ce tour - Si l’addon a un état (par exemple un artefact sélectionné), le consommer dans
augmentPrompt— le deuxième appel retourneranull - Ne pas lancer d’exception — retourner
nullen cas d’erreur non fatale
Conventions de stockage
Section intitulée « Conventions de stockage »Les addons écrivent dans .arka-deck/addons/<addon-name>/ à l’intérieur du dossier projet.
<project-root>/└── .arka-deck/ └── addons/ └── mon-addon/ ├── options.json └── logs/Règle : n’écrivez jamais en dehors de .arka-deck/addons/<votre-addon>/. Le port Filesystem applique une allowlist — les écritures hors périmètre sont rejetées.
Pour les données globales (non liées à un projet) : ~/.arka-deck/addons/<addon-name>/.
Types importables depuis core
Section intitulée « Types importables depuis core »import type { ArkaEvent, ArkaEventType } from '../../core/domain/events/arka-event.js';import type { EventBus } from '../../core/ports/outbound/event-bus.js';import type { Filesystem } from '../../core/ports/outbound/filesystem.js';import type { Clock } from '../../core/ports/outbound/clock.js';import type { IdGenerator } from '../../core/ports/outbound/id-generator.js';import type { BeforeTurnAugmenter } from '../../core/ports/outbound/before-turn-augmenter.js';Chemin relatif : depuis addons/<name>/src/, core/ est à ../../../core/.
Template de fiche publique
Section intitulée « Template de fiche publique »Pour rédiger une fiche d’addon destinée à un usage marketing/site public, utiliser le gabarit bilingue docs/addons/_TEMPLATE.md (sections : pitch court, hero, what it does, mechanics/flow, compatibility, stay tuned).
Ce gabarit est distinct des fiches dev (qui restent dans docs/dev/addons-firstparty/).