Addon contract — formal reference
This document describes the formal interface an addon must respect to integrate into arka-deck. It complements the write an addon tutorial with exact types and contractual guarantees.
Source of truth for types: core/ports/outbound/, core/domain/events/arka-event.ts.
manifest.json
Section titled “manifest.json”Each addon declares a manifest.json at its root.
{ "name": "my-addon", "version": "1.0.0", "kind": "capability", "description": "Short addon description.", "status": "active", "workers": ["worker-name-1"], "dependencies": [], "entrypoint": "src/index.ts"}| Field | Type | Values | Required |
|---|---|---|---|
name | string | kebab-case | yes |
version | string | semver | yes |
kind | string | "capability" / "orchestration" / "feature" / "convention" | yes |
description | string | max ~200 chars | yes |
status | string | "active" / "draft" | yes |
workers | string[] | attached worker names | yes (empty if none) |
dependencies | string[] | required other addon names | yes (empty if none) |
entrypoint | string | relative path from addon root | yes for capability / orchestration |
label | string | displayable label | no |
runtime | string | "cli" / "web" / "shared" | no |
tier | string | "free" / "standalone" / "cortex" / "squad-business" | no |
exposes | object | declarative metadata | no |
The executable schema is composition/addons/manifest-schema.ts.
kind values in the JSON manifest are aligned with the runtime AddonManifest.category category from the TypeScript registry. LLM providers are not an addon kind: they use the separate ProviderManifestSchema.
First-party loading
Section titled “First-party loading”At startup, composition loads first-party manifests via composition/addons/loader.ts and also checks addon ↔ worker references. Warnings are logged, but declarative loading does not replace explicit runtime wiring in the composition root.
Canonical path to add a first-party addon (SPEC-05, decision B)
Section titled “Canonical path to add a first-party addon (SPEC-05, decision B)”A first-party addon must NOT require a direct edit of composition/core-container.ts to be added to the registry. The canonical path is:
- Declare the addon module under
addons/<name>/with itsmanifest.json, aregister<Name>Addon(deps)(forAddon) or a runtime built by composition (forRegisteredRuntimeAddon). - Add the manifest to
composition/addons/index.ts→FIRST_PARTY_RUNTIME_ADDON_MANIFESTS(or, for a self-registeringAddon, to theADDONSarray). - Wire through
composition/addons/first-party-runtime.ts→registerFirstPartyRuntimeAddons(deps). This is the single entry point that the composition root calls; adding an addon requires only extending the deps record there and the manifests list above. - Expose HTTP routes (optional) via
composition/addons/<name>-routes.ts, mounted fromcomposition/web-container.ts.
The composition root must keep a single call site for addon wiring (registerFirstPartyRuntimeAddons). Direct register<Name>Addon(...) calls inside core-container.ts are tracked as a SPEC-05 deviation and listed in ROADMAP.md under “Chantiers de split tracés (SPEC-05)”.
Today the composition root still calls
registerCortexActionsAddonandregisterLocalMemoryAddondirectly because their runtimes depend on container-local builders that have not yet been extracted. The target is to push these into the canonical path through dedicated factory helpers incomposition/addons/.
Current public limit
Section titled “Current public limit”The addon registry is an internal first-party brick. It validates manifests and provides a stable contract, but arka-deck does not yet load an arbitrary third-party marketplace in production.
Consequence for public documentation: speak of “addon contract” or “first-party extensible addons”, not of an already-active public marketplace. Runtime additions stay explicitly wired in the composition root.
The registry accepts two distinct forms:
Addon: the registry callsregister(deps)and builds the runtime.RegisteredRuntimeAddon: the composition root has already built the runtime; the registry stores it as-is and does not callregister(deps).
This distinction avoids presenting already-wired addons (cortex-actions, memory-local, mission-guardian) as if they were built by the registry.
Register function
Section titled “Register function”The addon entry point is a register<Name>Addon(deps) function exported from src/index.ts.
export interface MyAddonDeps { readonly clock: Clock; readonly filesystem: Filesystem; readonly eventBus: EventBus; readonly idGenerator?: IdGenerator; readonly resolveSession: (sessionId: string) => Promise<SessionContext | null>; readonly cortexBaseUrl?: string;}
export interface MyAddonRuntime { readonly forMyAddon: ForMyAddon; readonly beforeTurnAugmenter: BeforeTurnAugmenter; readonly unsubscribe: () => void;}
export function registerMyAddon(deps: MyAddonDeps): MyAddonRuntime { // 1. Instantiate adapters // 2. Build use-cases // 3. Subscribe to the bus // 4. Build the augmenter (if applicable) return { forMyAddon, beforeTurnAugmenter, unsubscribe };}Rule: unsubscribe() must always be present and idempotent. It is called at shutdown.
Available dependencies
Section titled “Available dependencies”Composition (composition/core-container.ts) provides these dependencies to each addon:
| Dep | Type | Source | Usage |
|---|---|---|---|
clock | Clock | core/ports/outbound/clock.ts | Current timestamp (clock.now()) |
filesystem | Filesystem | core/ports/outbound/filesystem.ts | File read/write (allowlist) |
eventBus | EventBus | core/ports/outbound/event-bus.ts | Subscribe and publish events |
idGenerator | IdGenerator | core/ports/outbound/id-generator.ts | Generate stable IDs |
resolveSession | (sessionId) => Promise<Context|null> | Composition | Resolve projectPath from sessionId |
cortexBaseUrl | string? | Env ARKA_DECK_CORTEX_URL | Cortex URL override for tests |
EventBus
Section titled “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;}Guarantees:
- An error in one handler does not impact others (isolation via
allSettled) - Handler order is not guaranteed
unsubscribeis idempotent- The bus is not persistent — events produced before subscription are not replayable
For the detailed guide, see ../architecture/event-bus.
Event catalogue (ArkaEvent)
Section titled “Event catalogue (ArkaEvent)”Source of truth: core/domain/events/arka-event.ts. Same categories as the French version: workspace, project, provider, catalogue/installs, chat session/turn/blocks/tools/compaction/slash/runtime, memory, settings, system, addons/workers, squad orchestration.
See the French version for the full type list — names are identical.
BeforeTurnAugmenter
Section titled “BeforeTurnAugmenter”Port core/ports/outbound/before-turn-augmenter.ts. Implement this port if your addon must inject context into a chat turn.
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;}Expected behavior:
- Return
nullif no augmentation for this turn - If the addon has state (e.g. a selected artifact), consume it in
augmentPrompt— the second call will returnnull - Do not throw — return
nullon non-fatal errors
Storage conventions
Section titled “Storage conventions”Addons write under .arka-deck/addons/<addon-name>/ inside the project folder.
<project-root>/└── .arka-deck/ └── addons/ └── my-addon/ ├── options.json └── logs/Rule: never write outside .arka-deck/addons/<your-addon>/. The Filesystem port enforces an allowlist — writes outside this perimeter are rejected.
For global (non-project) data: ~/.arka-deck/addons/<addon-name>/.
Types importable from core
Section titled “Types importable from 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';Relative path: from addons/<name>/src/, core/ is at ../../../core/.
Public sheet template
Section titled “Public sheet template”To write an addon sheet for marketing/public-site usage, use the bilingual template docs/addons/_TEMPLATE.md (sections: short pitch, hero, what it does, mechanics/flow, compatibility, stay tuned).
This template is distinct from dev sheets (which stay under docs/dev/addons-firstparty/).