Aller au contenu

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.


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"
}
ChampTypeValeursObligatoire
namestringkebab-caseoui
versionstringsemveroui
kindstring"capability" / "orchestration" / "feature" / "convention"oui
descriptionstringmax ~200 charsoui
statusstring"active" / "draft"oui
workersstring[]noms des workers rattachésoui (vide si aucun)
dependenciesstring[]noms d’autres addons requisoui (vide si aucun)
entrypointstringchemin relatif depuis la racine de l’addonoui pour capability / orchestration
labelstringlabel affichablenon
runtimestring"cli" / "web" / "shared"non
tierstring"free" / "standalone" / "cortex" / "squad-business"non
exposesobjectmétadonnées déclarativesnon

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.

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.

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 appelle register(deps) et construit le runtime.
  • RegisteredRuntimeAddon : la composition root a déjà construit le runtime ; le registre le stocke tel quel et ne rappelle pas register(deps).

Cette distinction évite de présenter les addons déjà câblés (cortex-actions, gouvernance-lite, memory-local) comme s’ils étaient construits par le registre.


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.


La composition (composition/core-container.ts) fournit ces dépendances à chaque addon :

DepTypeSourceUsage
clockClockcore/ports/outbound/clock.tsTimestamp actuel (clock.now())
filesystemFilesystemcore/ports/outbound/filesystem.tsLecture/écriture fichiers (allowlist)
eventBusEventBuscore/ports/outbound/event-bus.tsS’abonner et publier des events
idGeneratorIdGeneratorcore/ports/outbound/id-generator.tsGénérer des IDs stables
resolveSession(sessionId) => Promise<Context|null>CompositionRésoudre projectPath depuis sessionId
cortexBaseUrlstring?Env ARKA_DECK_CORTEX_URLOverride URL Cortex pour tests

core/ports/outbound/event-bus.ts
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
  • unsubscribe est 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.


Source de vérité : core/domain/events/arka-event.ts.

TypeChamps clés
workspace.createdworkspaceId, name, createdAt
workspace.updatedworkspaceId, name, description
workspace.deletedworkspaceId
TypeChamps clés
project.createdprojectId, projectPath, workspaceId, name
project.updatedprojectId, name, workspaceId
project.forgottenprojectId
project.purgedprojectId, projectPath, deletedPaths
project.last-usedprojectId, lastUsedAt
project.detected-by-cwdprojectId, projectPath, cwd
TypeChamps clés
provider.instance.createdinstanceId, manifestId, modelId, isDefault
provider.instance.updatedinstanceId, changes
provider.instance.deletedinstanceId
provider.instance.testedinstanceId, kind, ok, latencyMs, error?
TypeChamps clés
catalogue.refreshedsource, entriesCount
agent.installedprojectPath, sourceId, version, localPath
agent.removedprojectPath, sourceId
agent.updatedprojectPath, sourceId, fromVersion, toVersion
agent.relinkedprojectPath, sourceId
agent.divergence-detectedprojectPath, transitions, items
TypeChamps clés
chat.session.startedsessionId, projectId, projectPath?, agentId?, runtimeId, modelId, createdAt
chat.session.resumedsessionId, runtimeSessionId?
chat.session.endedsessionId, projectPath?, transcript, reason, endedAt
chat.session.supersededsessionId, supersededBySessionId, projectId, agentId, archivedAt
chat.session.renamedsessionId, name
chat.session.deletedsessionId
chat.session.preparing.startedsessionId, projectId, agentId?, startedAt
chat.session.preparing.endedsessionId, runtimeSessionId?, durationMs, usage?, endedAt
chat.session.preparing.failedsessionId, error, durationMs, failedAt
TypeChamps clés
chat.turn.startedsessionId, userText, attachmentsCount, startedAt
chat.turn.endedsessionId, success: true, durationMs, usage?
chat.turn.abortedsessionId, reason, error?
TypeChamps clés
chat.block.startedsessionId, blockId, kind, tool?
chat.block.endedsessionId, blockId, kind, content?
TypeChamps clés
chat.tool.calledsessionId, toolUseId, tool, input
chat.tool.resultsessionId, toolUseId, status
chat.tool.approval-requestedsessionId, requestId, tool, input
chat.tool.approval-decidedsessionId, requestId, decision, message?
TypeChamps clés
chat.compact.presessionId, projectPath?, transcript
chat.compact.postsessionId
TypeChamps clés
chat.slash-command.detectedsessionId, commands
chat.memorizesessionId, projectPath?, text
chat.recap-projectsessionId, projectPath
chat.memorize-learningsessionId, projectPath?, text
TypeChamps clés
chat.attachment.addedsessionId, kind, mediaType, sizeBytes
chat.runtime.session-id-capturedsessionId, runtimeSessionId
chat.runtime.failuresessionId, projectPath?, error, transcript
TypeChamps clés
memory.entry.capturedprojectPath, entryId, kind, health, trigger
memory.recall-pack.composedprojectPath, tokens, entriesCount
memory.learning.proposedprojectPath, learningId
memory.learning.validatedprojectPath, learningId
memory.degradedprojectPath, reason
TypeChamps clés
settings.preferences.changedkey, value
settings.launch-options.changedprojectId, key, value
system.boot.completedversion, addons
system.online-state.changedonline
system.shutdown.requestedreason
addon.registeredaddonId, kind, version
worker.registeredworkerId, addonParent, runtime
squad.mission.event-emittedmissionId, projectPath, sequence, persistedAt, event
orchestration.run.event-emittedrunId, projectPath, sequence, persistedAt, event

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 null si 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 retournera null
  • Ne pas lancer d’exception — retourner null en cas d’erreur non fatale

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>/.


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/.


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/).