> ## Documentation Index
> Fetch the complete documentation index at: https://sdk.ekanos.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Configuration

> Build your harness.config.ts registry entry, defining widgets, fixtures, and live mode behavior.

The `harness.config.ts` file at your project root is the only file you edit. It exports a `harnessIntegrations` array, wrapped in `defineHarnessConfig()`, that the generated shell passes into every route.

## Registry structure

```ts theme={null}
import { defineHarnessConfig } from '@ekanos/harness/config';
import {
  harnessWidgetsFromDefinition,
  asHarnessDefinition,
} from '@ekanos/harness/registry';

export const harnessIntegrations = defineHarnessConfig([
  {
    // Required
    slug: 'my-integration',
    name: 'My Integration',
    description: 'Description shown on the index page',
    widgets: [],

    // Optional
    fixtures: {/* … */},
    tile: MyTile,
    activationForm: MyActivationForm,
    definition: asHarnessDefinition(myDefinition),
    triggerMocks: {/* … */},
    live: {/* … */},
    oauth: {/* … */},
  },
]);
```

## Fields

### Required

<ParamField body="slug" type="string" required>
  URL segment (and the id you would ship as your product slug). Must start with
  a letter or digit and contain only letters, digits, dot, underscore, or
  hyphen. `defineHarnessConfig()` checks this and rejects duplicate slugs.
</ParamField>

<ParamField body="name" type="string" required>
  Display name shown in headers and the index page.
</ParamField>

<ParamField body="description" type="string" required>
  Short description shown on the index page.
</ParamField>

<ParamField body="widgets" type="HarnessWidget[]" required>
  Array of widgets to render. May be empty.
</ParamField>

### Optional

<ParamField body="fixtures" type="Partial<Record<FixtureVariant, HttpFixture[]>>">
  Recorded HTTP exchanges per variant: `default`, `empty`, `error`. See
  [Fixtures](/harness/fixtures).
</ParamField>

<ParamField body="tile" type="ComponentType<MarketplaceTileProps>">
  Your marketplace card component, rendered in the catalogue grid on the
  Marketplace tile surface. If omitted, that surface says the integration
  registers no tile.
</ParamField>

<ParamField body="activationForm" type="ComponentType<ActivationFormProps>">
  Your activation/connection form. Rendered with `inline` on the Activation
  surface so it does not open a dialog.
</ParamField>

<ParamField body="definition" type="IntegrationDefinition">
  Your validated `defineIntegration()` output. Powers the Triggers surface, the
  egress allowlist, and actions. Wrap it with `asHarnessDefinition()` to erase
  the storage generic.
</ParamField>

<ParamField body="triggerMocks" type="HarnessTriggerMocks">
  Mock context for Triggers: storage, secrets, and dynamic fetch handlers. See
  [Triggers](/harness/triggers).
</ParamField>

<ParamField body="live" type="HarnessLiveMode">
  Opt in to live mode. Enables the toolbar's **Live API** option. See [Live
  Mode](/harness/live-mode).
</ParamField>

<ParamField body="oauth" type="{ connected?: boolean | Partial<Record<FixtureVariant, boolean>>; setup?: { consoleUrl?: string; consoleLabel?: string; notes?: string[] } }">
  Fixture-mode OAuth connection state (defaults to connected) and extra hints
  for the Local OAuth setup checklist. See [Triggers](/harness/triggers#oauth).
</ParamField>

## Widgets

### HarnessWidget

```ts theme={null}
interface HarnessWidget {
  id: string; // URL segment on /[slug]/widgets/[id]
  title: string; // Header chrome
  component: ComponentType<IntegrationComponentProps>;
  width?: 'half' | 'full'; // Grid column span; default: 'half'
  isCollapsible?: boolean; // Render collapse button
  isPinnable?: boolean; // Reach WidgetContext.meta.isPinnable
  aiFooterEnabled?: boolean; // Render "Ask about this" bar
  seeds?: Partial<Record<FixtureVariant, FixtureSeed[]>>; // Query-key seeds, fast path
}
```

<Note>
  All three chrome flags (`isCollapsible`, `isPinnable`, `aiFooterEnabled`) are
  opt-in: omit one and it is off, matching the host's default. With `isPinnable`
  on, `Widget.Header`'s ⋯ menu offers "Pin to board" / "Unpin from board".
</Note>

### Derive from definition

To avoid duplication, use `harnessWidgetsFromDefinition()`:

```ts theme={null}
import { harnessWidgetsFromDefinition } from '@ekanos/harness/registry';

widgets: harnessWidgetsFromDefinition(myDefinition, {
  'widget-1-id': { width: 'full' },
  'widget-2-id': {
    width: 'half',
    seeds: {
      default: [{ queryKey: myKey, data: myData }],
    },
  },
}),
```

The definition supplies `id`, `title` (from the widget's `name`), `component`, and any chrome flags it declares. The overrides map, keyed by widget id, supplies harness-only fields (`width`, `seeds`) and wins where both speak. An override key that matches no declared widget throws. Pass your definition directly; this helper does not need `asHarnessDefinition()`.

### Manual form

If you have scratch widgets or variants not yet in your definition, spread the helper result:

```ts theme={null}
widgets: [
  ...harnessWidgetsFromDefinition(myDefinition, {}),
  {
    id: 'scratch-widget',
    title: 'Experimental',
    component: ExperimentalWidget,
  },
],
```

<Warning>
  Widget sizing is two different systems. `layouts` (lg/md/sm rectangles in the
  definition) is what the real dashboard reads. `HarnessWidget.width`
  ('half'/'full') is what the harness reads. Set both and expect neither to
  validate the other.
</Warning>

## Fixtures

See [Fixtures](/harness/fixtures) for the full guide on `HttpFixture` and `FixtureSeed`.

```ts theme={null}
fixtures: {
  default: [
    { request: 'GET https://api.example.com/v1/data', response: { items: [/*…*/] } },
  ],
  empty: [
    { request: 'GET https://api.example.com/v1/data', response: { items: [] } },
  ],
  error: [
    { request: 'GET https://api.example.com/v1/data', status: 503 },
  ],
}
```

Variants sit on the **integration**, not the widget, because a URL is not owned by one widget.

## Live mode

See [Live Mode](/harness/live-mode) for full details.

```ts theme={null}
live: {
  // Do NOT set egress here if definition is set
  seeds: [
    { queryKey: settingsKey, data: mySettings },
  ],
  // Optional: open in live mode until the toolbar is changed. Use it when
  // your widgets never call the vendor from the browser.
  default: 'live',
}
```

## Triggers

See [Triggers](/harness/triggers) for full details.

```ts theme={null}
definition: asHarnessDefinition(myDefinition),

triggerMocks: {
  storage: {
    account: { config: { merchantId: 'test_123' } },
    user: {},
  },
  secrets: {
    account: { api_key: 'test_key_abc' },
  },
  fetchHandlers: [
    {
      match: 'https://dynamic.example.com',
      respond: () => Response.json({ dynamic: true }),
    },
  ],
},

oauth: {
  setup: {
    consoleUrl: 'https://developer.example.com',
    consoleLabel: 'Example Developer Console',
    notes: ['Client ID is your app key'],
  },
  connected: { default: true, empty: false },
},
```

## Validation errors

### Duplicate slug

```
defineHarnessConfig(): duplicate slug "my-integration" at entries 0 and 1. Every surface is routed by slug, so the later entry would be permanently unreachable and the two would collide as React keys on the index page.
```

### Duplicate widget id

```
defineHarnessConfig(): duplicate widget id "my-widget" in "my-integration". Widget ids address the single-widget surface, so the second one could never be opened.
```

### Invalid slug or widget id

```
defineHarnessConfig(): widget id "my/widget" (slug "my-integration") is not usable as a URL segment. It must start with a letter or digit and contain only letters, digits, dot, underscore or hyphen — the harness routes every surface at /<slug>/…, so a slash, space or "#" here silently produces a page that 404s.
```

The same message is raised for a slug, with `slug` and `(entry <index>)` in place of the widget id.

### Widget override mismatch

```
harnessWidgetsFromDefinition(): "wrong-id" is not a widget in "my-integration". Declared: my-widget. An override keyed to a widget that does not exist is silently ignored otherwise, which reads as the override not working.
```

Check the widget id spelling in your override map.

## Constants

Widgets are rendered with these ids, exported from the registry entry:

```ts theme={null}
import {
  HARNESS_ACCOUNT_ID, // '00000000-0000-4000-8000-000000000001'
  HARNESS_SOURCE_ID, // '00000000-0000-4000-8000-000000000002'
  HARNESS_ACCOUNT_SLUG, // 'harness-team'
} from '@ekanos/harness/registry';
```

They are plausible UUIDs rather than sentinels, so a widget that validates the shape of its `accountId` accepts them. Nothing reads them; there is no database.

## Shell contract

The harness declares `ekanos.shellContract` in its `package.json` (currently `2`). `ekanos dev` reads it before generating the shell and refuses if its templates produce a different contract, naming which side to upgrade. The contract covers the generated shell only, not your `harness.config.ts`, which is never regenerated — so a rename in the registry types reaches you as a TypeScript error. Read the release notes on minor bumps.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.