Skip to content

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.


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"
}
FieldTypeValuesRequired
namestringkebab-caseyes
versionstringsemveryes
kindstring"capability" / "orchestration" / "feature" / "convention"yes
descriptionstringmax ~200 charsyes
statusstring"active" / "draft"yes
workersstring[]attached worker namesyes (empty if none)
dependenciesstring[]required other addon namesyes (empty if none)
entrypointstringrelative path from addon rootyes for capability / orchestration
labelstringdisplayable labelno
runtimestring"cli" / "web" / "shared"no
tierstring"free" / "standalone" / "cortex" / "squad-business"no
exposesobjectdeclarative metadatano

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.

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.

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 calls register(deps) and builds the runtime.
  • RegisteredRuntimeAddon: the composition root has already built the runtime; the registry stores it as-is and does not call register(deps).

This distinction avoids presenting already-wired addons (cortex-actions, gouvernance-lite, memory-local) as if they were built by the registry.


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.


Composition (composition/core-container.ts) provides these dependencies to each addon:

DepTypeSourceUsage
clockClockcore/ports/outbound/clock.tsCurrent timestamp (clock.now())
filesystemFilesystemcore/ports/outbound/filesystem.tsFile read/write (allowlist)
eventBusEventBuscore/ports/outbound/event-bus.tsSubscribe and publish events
idGeneratorIdGeneratorcore/ports/outbound/id-generator.tsGenerate stable IDs
resolveSession(sessionId) => Promise<Context|null>CompositionResolve projectPath from sessionId
cortexBaseUrlstring?Env ARKA_DECK_CORTEX_URLCortex URL override for 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;
}

Guarantees:

  • An error in one handler does not impact others (isolation via allSettled)
  • Handler order is not guaranteed
  • unsubscribe is idempotent
  • The bus is not persistent — events produced before subscription are not replayable

For the detailed guide, see ../architecture/event-bus.


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.


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 null if no augmentation for this turn
  • If the addon has state (e.g. a selected artifact), consume it in augmentPrompt — the second call will return null
  • Do not throw — return null on non-fatal errors

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


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


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