# Config manifest (/docs/config-manifest)



The config manifest records *which substrate ran*. A
[fingerprint](/docs/fingerprint) binds a turn's inputs to a hash;
the manifest binds the substrate that processed them — the plugin
set, the system prompts, the graph node bodies, the channel
definitions, the post-stage filters. One row per distinct
substrate, content-addressable, written to `harness_config_manifest`.

Every [event-log](/docs/event-log) row carries a `manifest_hash`
foreign reference to that row. So a full replay of any historical
session needs exactly two things: the event stream and the
manifest hash that was active when each event fired. That tuple is
self-contained — no live runtime, no ambient git HEAD, no
deploy-record lookup.

<StatusBadge status="in-flight">
  substrate contract locked · rolling out
</StatusBadge>

<SourceMeta
  subpath="@pleach/core/schema"
  source="[
  { label: &#x22;src/schema/postgres/&#x22;, href: &#x22;https://github.com/pleachhq/core/tree/main/src/schema/postgres&#x22; },
  { label: &#x22;src/eventLog/hashChain.ts&#x22;, href: &#x22;https://github.com/pleachhq/core/blob/main/src/eventLog/hashChain.ts&#x22; },
]"
/>

<Callout title="Rolling out additively">
  The `harness_config_manifest` table and the
  `harness_event_log.manifest_hash` column land as additive schema
  files — `manifest_hash` is nullable during rollout so rows written
  before the manifest substrate stay valid. Existing sessions keep
  working; new sessions stamp the reference. The cutover to a
  required column is a follow-up migration after every write path
  stamps the hash. See [Schema](/docs/schema) for the bundle file.
</Callout>

## Why it exists [#why-it-exists]

[Determinism](/docs/determinism) promises a turn replays
byte-identical against the same package version and the same input.
"Same package version" is the load-bearing phrase — but a
deployment's *effective* substrate is more than the npm version. It
includes the plugins you registered, the prompt templates you
shipped, the graph nodes your build composed, the channels and
filters in play. Two deployments on the same `@pleach/core`
version can run different substrates.

The fingerprint doesn't capture that. It binds the turn input. The
manifest captures the rest, so the replay contract becomes precise:
&#x2A;*same manifest, same input, same output.**

## The five surfaces [#the-five-surfaces]

The manifest is a snapshot of five substrate axes. Each is
content-hashed independently, then rolled up into one top-level
hash.

| Surface  | Column             | What it captures                                                   |
| -------- | ------------------ | ------------------------------------------------------------------ |
| Plugins  | `plugin_manifest`  | The registered plugin set and the hooks each contributes           |
| Prompts  | `prompt_manifest`  | The system-prompt facets and templates in effect                   |
| Nodes    | `node_manifest`    | The graph node bodies (by content hash) and their stage assignment |
| Channels | `channel_manifest` | The channel (state) definitions and their reducers                 |
| Filters  | `filter_manifest`  | The post-stage filter declarations                                 |

Each surface gets its own child hash (`plugin_hash`,
`prompt_hash`, …). Per-surface hashes are what make the manifest
dedup well: in a multi-tenant deployment most tenants load the
same plugin set, so the `plugin_manifest` blob is shared across
their manifest rows even when their prompts differ.

## The table [#the-table]

```sql
CREATE TABLE harness_config_manifest (
  manifest_hash    TEXT PRIMARY KEY,   -- top-level Merkle roll-up
  plugin_hash      TEXT NOT NULL,      -- per-surface child hash
  prompt_hash      TEXT NOT NULL,
  node_hash        TEXT NOT NULL,
  channel_hash     TEXT NOT NULL,
  filter_hash      TEXT NOT NULL,
  plugin_manifest  JSONB NOT NULL,     -- full snapshot, not a delta
  prompt_manifest  JSONB NOT NULL,
  node_manifest    JSONB NOT NULL,
  channel_manifest JSONB NOT NULL,
  filter_manifest  JSONB NOT NULL,
  reference_count  INTEGER NOT NULL DEFAULT 1,   -- live event references
  created_at       TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  tenant_id        TEXT NOT NULL DEFAULT 'default'
);
```

The primary key is the content hash, so two sessions that ran the
same substrate write the same row — the insert is
`ON CONFLICT DO NOTHING`. Each per-surface hash gets its own index
for the cross-tenant query shapes below, plus a `tenant_id` index
for per-tenant scans.

Row-level security ships the same two-policy template as the rest
of the [schema bundle](/docs/schema#rls-template): a service-role
bypass for server-side adapters, and a `tenant_id = current_tenant()`
isolation policy for scoped clients.

### Foreign reference on the event log [#foreign-reference-on-the-event-log]

```sql
ALTER TABLE harness_event_log
  ADD COLUMN manifest_hash TEXT                  -- nullable during rollout
    REFERENCES harness_config_manifest (manifest_hash)
    ON DELETE SET NULL
    DEFERRABLE INITIALLY DEFERRED;

CREATE INDEX harness_event_log_manifest_hash_idx
  ON harness_event_log (manifest_hash);
```

The event log carries only the *top-level* `manifest_hash`; the
per-surface child hashes live on the manifest row, reached by
joining. The column is a real foreign key, but two clauses keep it
cheap and rollout-safe:

* **`DEFERRABLE INITIALLY DEFERRED`** is load-bearing. A session
  writes its manifest fire-and-forget and writes its first event in
  the same transaction; deferring the constraint to commit time lets
  those two writes race without a transient FK violation.
* **`ON DELETE SET NULL`** means a retention GC that drops a manifest
  nulls the reference rather than cascading a delete into the audit
  trail — the event survives, it just loses its substrate pointer.

The constraint is nullable during rollout so rows written before the
manifest substrate stay valid. Two gates back the reference:
`audit:config-manifest-fk-constraint-applied` asserts the migration
carries every canonical clause, and
`audit:config-manifest-referential-integrity` asserts zero orphans
online (and the `SessionRuntime` wiring offline).

## The hash [#the-hash]

Each surface is serialized to canonical JSON
([RFC 8785](https://www.rfc-editor.org/rfc/rfc8785) — sorted keys,
no insignificant whitespace) and hashed with SHA-256. The
top-level hash is a Merkle roll-up over the five child hashes,
ordered, with a versioned prefix:

```
manifest_hash = sha256_hex(
  "pleach.manifest.v1" ␟
  "plugin:"  ␟ plugin_hash  ␟
  "prompt:"  ␟ prompt_hash  ␟
  "node:"    ␟ node_hash    ␟
  "channel:" ␟ channel_hash ␟
  "filter:"  ␟ filter_hash
)
```

`␟` is the ASCII unit separator (`0x1F`). This mirrors the
[hash chain](/docs/hash-chain)'s `pleach.c9.v1` canonicalization —
same separator, same versioned-prefix discipline, same hex output
shape. The version prefix lets a future schema add a sixth surface
without colliding with `v1` manifests.

The manifest builder is a pure function of its inputs — no
`Date.now()`, no environment reads — so the same substrate always
produces the same hash. The `audit:plugin-content-hash-stability`
gate runs two consecutive builds and diffs each of the five child
hashes independently; a `Date.now()` leak in the prompt builder
fails `prompt_hash` and points at the prompt surface specifically,
not at "something in the manifest moved."

## Querying [#querying]

The manifest turns three audit questions into index-backed
queries.

**Replay by config** — every event under the substrate active in a
session:

```sql
SELECT events.*
  FROM harness_event_log events
  WHERE events.manifest_hash = $1
  ORDER BY events.sequence_number;
```

**Cross-tenant audit by plugin set** — every event across all
sessions that ran with a given plugin set, regardless of prompt,
node, channel, or filter drift:

```sql
SELECT events.*
  FROM harness_event_log events
  JOIN harness_config_manifest manifest
    ON events.manifest_hash = manifest.manifest_hash
  WHERE manifest.plugin_hash = $1;
```

**Substrate reconstruction** — the full substrate behind a single
event, all five surfaces returned as JSONB:

```sql
SELECT manifest.*
  FROM harness_config_manifest manifest
  JOIN harness_event_log events
    ON events.manifest_hash = manifest.manifest_hash
  WHERE events.id = $1
  LIMIT 1;
```

A manifest lookup is a primary-key hit, so JOIN cost is dominated
by the event-log scan, not the manifest cardinality. A typical
incident window of a thousand events references only a handful of
distinct manifests.

## Full snapshots, not deltas [#full-snapshots-not-deltas]

Each row stores the complete substrate, not a diff against a prior
manifest. The storage cost is small — content-addressing dedups
identical substrates, and per-surface hashes dedup the shared
parts — and the payoff is that replay reads one row. A delta
encoding would force the replay tool to walk a parent chain, which
breaks the moment retention drops a parent, and it would turn the
round-trip audit into a multi-step materialization. One row per
manifest keeps replay offline-able.

## Retention [#retention]

Manifests are reference-counted against the event log: a manifest
survives as long as any event references it. The default floor is
**forever** — the compliance posture is "keep the audit trail,"
not "expire it." Cost-sensitive tenants can opt into a per-tenant
retention floor (planned) that drops manifests older than *N* days
*and* referenced by no surviving event.

This couples manifest retention to whatever event-log retention
you run, automatically:

| Event-log retention | Manifest retention                                            |
| ------------------- | ------------------------------------------------------------- |
| Forever             | Forever                                                       |
| *N*-day TTL         | Manifest drops within a GC pass of the last referencing event |
| Per-tenant policy   | Manifest follows per tenant                                   |

The `audit:config-manifest-retention-completeness` gate runs after
a GC pass and fails if any event references a missing manifest —
the guard against a GC racing a concurrent event write.

## One snapshot per session [#one-snapshot-per-session]

The manifest is captured once, at session construction — never
mid-stream. The content-addressable contract requires a stable
snapshot, so a config change mid-session is out of scope: the
session represents the substrate it started with. Mutating
manifest-relevant state mid-session is the kind of thing the
replayability contract is designed to forbid, not silently absorb.

## Where it sits in the replay story [#where-it-sits-in-the-replay-story]

<Mermaid
  chart="flowchart LR
    Input([turn input]) --> FP[computeFingerprint]
    Substrate([plugins · prompts · nodes · channels · filters]) --> Manifest[manifest_hash]
    FP --> Event[(event-log row)]
    Manifest --> Event
    Event --> Replay[offline replay]"
/>

The fingerprint binds the inputs; the manifest binds the
substrate; the event-log row carries both. Replay reads the
stream, dereferences each `manifest_hash`, reconstructs the
substrate, and re-runs the events against it. Same manifest, same
input, same output.

## Where to go next [#where-to-go-next]

<Cards>
  <Card title="Schema" href="/docs/schema" description="The schema bundle this table lands in, and the RLS template it shares." />

  <Card title="Event log" href="/docs/event-log" description="The stream whose rows carry the `manifest_hash` reference." />

  <Card title="Determinism" href="/docs/determinism" description="The replay contract the manifest completes — same manifest, same input, same output." />

  <Card title="Fingerprint" href="/docs/fingerprint" description="The input-binding half; the manifest is the substrate-binding half." />
</Cards>
