# Greenfield Destination Safety Contract

| Field | Contract value |
| --- | --- |
| Status | Phases 0-6 implemented in development; Phase 7 documentation contract recorded; unchanged-candidate performance evidence, clean-host installation evidence, recovery drill, and final release approvals remain pending |
| Contract ID | `destination-safety-v1` |
| Contract revision | `1` |
| Drafted on | 2026-09-01 |
| Locked on | 2026-09-01 |
| Supported PostgreSQL majors | `15`, `16`, `17`, and `18` |
| Installation model | Greenfield pgpipe control-plane installation only |
| Protected schema | `pgpipe_safety` |
| Runtime schema | `pgpipe_runtime` |
| Protected owner | `pgpipe_safety_owner` |
| Runtime role | The configured destination login; examples use `pgpipe_writer` |

This document is the normative contract for the greenfield destination-safety
redesign. It locks ownership, privileges, object shape, initialization,
readiness, failure behavior, and credential handling before SQL or runtime
behavior changes.

The development tree implements the Phase 1 protected-storage kernel, Phase 2
runtime integration, and Phase 3 `pgpipe init` browser finalization. A single
versioned manifest now supplies every
protected runtime relation name in `pgpipe_safety`; each managed destination
heartbeat is routed to a stable pipeline-scoped
`pgpipe_runtime.heartbeat_<pipeline-hash>` relation; and both namespaces are
reserved against customer mappings. Startup and pre-repair admission perform
fresh read-only validation. Runtime code cannot create, alter, upgrade, grant
on, or repair protected storage, while repair chunks retain their transactional
physical-relation and fence checks. `pgpipe repair check` is a runtime-role,
read-only diagnostic. None of these checks run per replicated event.

`pgpipe init` now holds a protected pending candidate, accepts a temporary DBA
username/password on its final browser page, prepares and validates the exact
destination manifest, validates the runtime role, and publishes the canonical
configuration with repair disabled. Phase 4's seven-state dashboard lifecycle,
fresh enablement check, restart-required activation, immediate admission/claim
quiesce on disable, and identity-bound post-start full-manifest watcher are
implemented in development. Phase 5 adds deterministic fresh-install paths: a
fresh `make demo-up` performs the setup-only Compose-secret bootstrap and leaves
repair off, while DEB/RPM/raw installs use the same browser finalization before
systemd may start. Package transactions never request database credentials,
and only the explicit `make demo-reset` target deletes demo volumes. Phase 6
executable security, regression, browser, and paired-performance gates are
implemented. Current-candidate repeated performance evidence, clean Ubuntu/RPM
host execution, the operator recovery drill, and final release approvals remain
pending under the Phase 7 certification register in section 15. A statement in an older plan about
a shared `pgpipe` schema, administrator-owned safety tables, `pgpipe repair
setup`, legacy adoption, in-place migration, or backward compatibility cannot
override this contract.

The words **MUST**, **MUST NOT**, **SHOULD**, and **MAY** are normative.

## 1. Scope and non-goals

### 1.1 Supported installation

A supported installation has no published pgpipe configuration, no pgpipe
repair/control state, and no pre-contract destination safety schema. The
destination may already contain application tables and data; “greenfield”
refers only to pgpipe-owned control and safety state.

The ordinary first-run flow is:

1. Run `pgpipe init`.
2. Complete the temporary browser wizard.
3. The wizard prepares and validates destination safety storage.
4. The wizard publishes configuration with `repair.execution_enabled: false`.
5. Start pgpipe and verify normal replication.
6. An authenticated dashboard administrator may later enable repair and
   restart pgpipe.

There is no repair-specific setup command in the supported fresh-install path.

### 1.2 Explicitly excluded

This design does not include:

- in-place migration of an older safety schema;
- ownership transfer of older pgpipe objects;
- adoption or projection of legacy repair jobs;
- mixed-version or rolling-client compatibility;
- backward reads of an older repair state format;
- automatic deletion of old objects or evidence;
- `REASSIGN OWNED`, broad grants, or an owner fallback;
- repair activation during initialization.

Existing development/demo objects are unsupported and disposable. Their
presence produces `repair_safety_reset_required`; pgpipe does not mutate or
delete them. A disposable demo is recreated only through the explicitly
destructive demo reset flow. A native development environment uses a new
destination database plus new configuration/state paths, or a separately
reviewed DBA reset outside pgpipe.

For the repository demo, the deterministic reset is the explicitly destructive
`make demo-reset`, followed by `make demo-up`. `demo-reset` destroys the demo
source, destination, pgpipe state/configuration, and repair history volumes;
`make demo-down` only stops the stack and preserves those volumes. A fresh
`demo-up` performs the demo-internal safety bootstrap and leaves repair
disabled. `demo-up` against stale volumes fails with the stable reset code and
never deletes them automatically.

This clean break applies to this destination-safety redesign. It does not
silently remove unrelated pgpipe package, configuration-path, or state-backend
maintenance contracts.

### 1.3 Future safety-schema revisions

Contract v1 has no in-place migration from pre-contract or development safety
objects. That greenfield rule is separate from how a future supported contract
revision will be delivered. Normal runtime startup, the dashboard, and package
installation scripts MUST never create or upgrade protected database objects.

A future release that changes the protected manifest MUST ship a separately
reviewed, explicit upgrade workflow. Its release-specific runbook must require:

1. stopping every old pgpipe process that could use the destination and
   prohibiting mixed old/new binaries;
2. recording an application maintenance window and taking a coordinated,
   restorable backup of the destination, pgpipe state, and required
   cluster-global role/parameter-ACL evidence;
3. running one bounded operation with a newly supplied temporary destination
   administrator credential—never the retained runtime credential;
4. proving the live destination identity and old manifest before mutation,
   applying the versioned change under destination-global fencing, and
   validating the complete new manifest before success;
5. discarding the administrator credential, reconnecting as the runtime role,
   and completing a read-only readiness probe before restart; and
6. retaining a sanitized upgrade receipt and the release-specific recovery
   procedure.

Until such a workflow exists for the exact future version, a manifest-version
mismatch fails closed. Operators must not rerun today's greenfield `pgpipe init`,
edit protected objects manually, grant ownership to `pgpipe_writer`, or expect a
DEB/RPM transaction to migrate the database.

## 2. Trust and responsibility model

These are seven different security identities, even when one person administers
more than one of them. A PostgreSQL role, dashboard account, and operating-system
account are not interchangeable merely because their names happen to match.

| Identity | Security realm and lifetime | Responsibility | Forbidden responsibility |
| --- | --- | --- | --- |
| Source replication role | Login on the source PostgreSQL system; retained runtime credential | Stream logical WAL, read selected source tables for snapshot/proof work, and perform only the separately documented publication/barrier operations | Has no destination, dashboard, local-service, or protected-owner authority |
| Destination application-table owner | Role chosen by the destination application/DBA; normally long lived | Own customer schemas/tables in the hardened DML-only profile, or explicitly delegate the bounded application-schema authority selected for pgpipe-managed DDL | Owns no `pgpipe_safety` object and cannot act as proof that repair safety is valid |
| Temporary bootstrap administrator | Privileged destination login used for one bounded `pgpipe init` finalization | Create or validate the fixed owner, schemas, protected manifest, grants, and runtime probe in the exact reviewed destination | Is not stored, reused by runtime, made the permanent safety owner, or accepted by the normal dashboard |
| `pgpipe_safety_owner` | Fixed destination PostgreSQL role; permanent `NOLOGIN` owner | Own `pgpipe_safety`, its seven tables, their dependent objects, and the exact replica-session activator | Cannot log in, be assumed through membership, own customer/runtime objects, or run the service |
| `pgpipe_writer` (configured destination runtime role) | Restricted destination PostgreSQL login; retained runtime credential | Apply customer-table writes and use only the manifest's exact protected DML/activator grants | Cannot own, alter, drop, truncate, create in, or delegate privileges on protected storage; cannot assume its owner |
| Dashboard administrator | Authenticated pgpipe application identity | Enable or disable repair, review exact plans, approve, monitor, and audit | Receives no PostgreSQL DBA credential or protected-object ownership authority |
| Linux service account (`pgpipe`) | Non-root operating-system identity | Read the protected runtime credential, own local configuration/state files, and run the pgpipe process | Is not by itself a PostgreSQL role, database administrator, or dashboard approval identity |

The role boundary protects safety evidence from accidental runtime DDL,
`TRUNCATE`, ownership transfer, and direct protected-ACL delegation. It does
not protect against a PostgreSQL superuser, a compromised DBA, or misuse of DML
that the runtime legitimately needs. Those are separate trust boundaries.

A compromised runtime can proxy an allowed DML capability—or the narrow
replica activator—through a new object in a schema where it has `CREATE`, or by
sharing its own credential. PostgreSQL ownership separation cannot prevent an
actor from relaying authority it already possesses. The contract prevents
direct protected-object ACL delegation and owner-level mutation; complete
manifest validation detects a forbidden proxy object and then blocks repair or
cancels the pipeline. It does not claim instantaneous prevention during the
watcher's bounded detection interval.

`NOLOGIN` is necessary because a PostgreSQL owner inherently has powers that
ordinary grants cannot remove, including changing or dropping its objects and
changing their ACLs. If the service could authenticate as that owner, a leaked
runtime password or defective DDL path could bypass the grant boundary. `NOLOGIN`
alone is not sufficient: the empty, non-administrable membership graph is what
also prevents `pgpipe_writer` or another login from reaching those owner powers
with `SET ROLE`. The service therefore authenticates only as the restricted
runtime role and exercises the small DML allowlist that the manifest requires.

### 2.1 Destination application-table ownership profiles

The protected-owner contract is identical in both supported application-table
profiles. This choice affects customer schemas and tables only; neither profile
grants `CREATE`, ownership, or grant options in `pgpipe_safety`, and the runtime
role remains denied database-level `CREATE`.

| Profile | Customer-table owner and runtime authority | Suitable capabilities | Deliberate limitations and risk |
| --- | --- | --- | --- |
| Hardened DML-only | The destination application owner retains schema/table ownership. The runtime receives schema `USAGE` and the reviewed table/sequence privileges required for configured replication and verification, normally `SELECT, INSERT, UPDATE, DELETE` on selected tables. | Streaming and repair against pre-created, schema-compatible destination objects | Automatic destination-table creation, destination reset/truncate, structured destination DDL, table replacement, and shadow-table rebuild are unavailable unless their additional privilege is granted. This minimizes the runtime credential's customer-DDL blast radius. |
| pgpipe-managed DDL | Use a dedicated application schema. Its owner deliberately grants the runtime `USAGE, CREATE` there and the table/sequence ownership or operation privileges required by the enabled snapshot, structured-DDL, reset, and rebuild features. Tables created by pgpipe are consequently owned by the runtime unless the DBA performs a separately controlled ownership transfer. | Automatic creation and the explicitly enabled, supported destination DDL/rebuild workflows | A compromised or defective runtime can affect customer definitions and any customer data reachable through that authority. Use a dedicated destination boundary, grant no broader database/schema power, and enable only the required DDL features. |

This is an installation privilege profile, not a repair-dashboard switch.
Preflight and the individual operation still fail closed when a required
customer-object privilege is absent. Operators MUST NOT grant broad ownership or
schema creation merely to silence such a failure; choose the intended profile
and disable incompatible managed-DDL/reset/rebuild behavior instead.

## 3. Fixed roles and schemas

### 3.1 Protected owner

The fixed role name is `pgpipe_safety_owner`. It MUST have exactly these
security-relevant attributes:

```text
NOLOGIN
NOINHERIT
NOSUPERUSER
NOCREATEDB
NOCREATEROLE
NOREPLICATION
NOBYPASSRLS
PASSWORD NULL (privileged-bootstrap attested; continuously enforce `NOLOGIN`)
CONNECTION LIMIT -1
VALID UNTIL infinity
```

It has the exact shared-role comment
`pgpipe protected destination safety owner v1` and no applicable
`pg_db_role_setting` entry in any database/wildcard scope. The runtime role's
comment and connection limit remain administrator-owned configuration and are
not changed by pgpipe.

The committed role graph contains no `pg_auth_members` row whose member is
`pgpipe_safety_owner` and no row whose granted role is
`pgpipe_safety_owner`. Therefore the owner inherits no other role and no login
can inherit, `SET ROLE` to, or administer it through membership. PostgreSQL 16
and newer `INHERIT`, `SET`, and `ADMIN` options are still inspected and must
not provide an indirect path through another role. PostgreSQL 15 treats any
reachable membership as exercisable and rejects it.

On every supported PostgreSQL major, the owner has exactly one direct parameter
privilege: `SET` on `session_replication_role`, granted without grant option.
`PUBLIC`, the runtime role, and every role reachable from runtime have no `SET`
or `ALTER SYSTEM` privilege on that parameter. This grant is cluster-wide, so
it is the same for every database that reuses the fixed owner. The owner is
`NOLOGIN`, has the empty membership graph above, and cannot be assumed; the sole
database-local way runtime can exercise the capability is the exact allowlisted
activator in its own initialized destination database.

On every supported PostgreSQL major (15-18), creating or reusing the fixed
owner requires a true
superuser or a managed-service equivalent that can create/alter/act as the role
and, where applicable, remove every automatically generated membership
regardless of grantor, inspect the full final catalog/data manifest after
temporary memberships are removed, and complete the exact negative probes.
That authority must also grant the protected owner the narrow parameter
capability and mark internal RI triggers `ENABLE ALWAYS`. Generic `CREATEROLE`
is unsupported on every supported major.
Stock
PostgreSQL 16+ also gives a non-superuser creator an ADMIN membership it cannot
remove, which cannot satisfy the empty final role graph.

PostgreSQL 14 is deliberately outside contract v1. In that version a login
role can grant membership in itself without having an explicit `ADMIN` option,
which would let the runtime login delegate all direct safety ACLs. PostgreSQL
15 removed that implicit self-administration. Contract v1 fails closed instead
of weakening its non-delegation promise for PostgreSQL 14.

On every supported major, the administrator must also be able to create schemas in the
destination database; create, comment, and grant on manifest objects;
temporarily act as the configured runtime role; inspect required catalogs; and
take the advisory lock. pgpipe tests these capabilities without retaining the
credential. If a managed provider exposes no authority meeting the applicable
rule, browser bootstrap fails with `repair_bootstrap_authority_insufficient`;
it does not leave a weaker owner graph or ask runtime to own safety storage.

When PostgreSQL requires membership to create an object directly under its
final owner, bootstrap creates a narrowly scoped temporary membership only
after proving that the administrator already has authority to grant it. For
the configured runtime role, PostgreSQL 16+ grants only the required `SET`
option with `INHERIT FALSE, ADMIN FALSE`; PostgreSQL 15 uses the older
membership form. Owner-role membership follows the stronger versioned
authority rule above. Every membership created automatically or explicitly
during bootstrap is revoked
before commit, and the final role graphs are revalidated. There is no fallback
to `postgres`, the supplied administrator, the runtime login, or another owner
name.

A pre-existing `pgpipe_safety_owner` may be shared by separate greenfield
pgpipe destination databases in the same PostgreSQL cluster. It is accepted
only when its observable attributes and cluster-wide membership graph already
match this contract, its current-database ownership closure contains no foreign
object, and its cluster-wide parameter ACL is already exact: the single direct
`SET` privilege without grant option on `session_replication_role`. Any other
difference returns `repair_bootstrap_owner_conflict`; bootstrap does not add, remove, or
repair a reused owner's role attributes, memberships, parameter ACL, or owned
objects. Only an owner newly created by the same initialization transaction
receives the parameter grant during that transaction.

Within the destination database, the owner's ownership closure is exact: it
owns only `pgpipe_safety`, the seven protected tables, the
replica-session activator, their table-dependent
indexes/constraints, and PostgreSQL-generated row/array types and TOAST
relations/indexes whose exact transitive dependency chain terminates at one of
those tables. The allowlisted chains are row type -> table, array type -> row
type -> table, TOAST relation -> table, and TOAST index -> TOAST relation ->
table. It MUST NOT own the database,
`pgpipe_runtime`, an application schema/table, a sequence, another
independently
created/custom type, collation,
conversion, routine, view, materialized view, publication, subscription,
foreign-data object, extension, or event trigger. No routine other than the
exact activator may be owned by it, especially no other `SECURITY DEFINER`
routine. A pre-existing role with any
extra owned object in the destination database is rejected rather than
cleaned up or adopted. Objects in another PostgreSQL database are outside this
database-local catalog proof and remain the DBA's responsibility; the
cluster-wide role attributes and membership graph are still validated.

Bootstrap creates a new owner with `PASSWORD NULL`. For an otherwise exact
pre-existing fixed owner, the sole allowed normalization is the idempotent
`ALTER ROLE pgpipe_safety_owner PASSWORD NULL`; this supports the same fixed
NOLOGIN owner across multiple fresh destination databases without preserving
an old login secret. No other attribute or membership is changed. When the
administrator can read `pg_authid`, bootstrap also verifies the stored null;
otherwise the successful privileged `CREATE/ALTER ROLE ... PASSWORD NULL`
operation is the attested proof recorded without secret material.
Runtime/startup validation does not falsely claim that an unprivileged
`pg_roles` read proves password absence: it revalidates observable `NOLOGIN`
and role attributes, while the privileged initialization receipt records the
password-null operation.

### 3.2 Runtime role

The runtime role is the configured destination login. Documentation and demo
configuration call it `pgpipe_writer`; installations MAY use another name.
The actual session identity, not merely the configured text, is validated.

It MUST be `LOGIN`, `NOSUPERUSER`, `NOCREATEDB`, `NOCREATEROLE`,
`NOREPLICATION`, and `NOBYPASSRLS`. It MUST be distinct from
`pgpipe_safety_owner`, MUST NOT own any protected object, and MUST NOT be able
to inherit, assume, administer, or re-grant the owner or bootstrap role.
It also has no incoming membership edge: no other role is a member of, may
`SET ROLE` to, or has `ADMIN` option on the runtime role. This prevents an
indirect caller from inheriting the replica activator's direct `EXECUTE` grant;
it does not prevent a compromised runtime from deliberately creating a proxy,
which is handled by the detection boundary above.
The complete role-membership graph reachable through inheritance or `SET ROLE`
MUST contain no role with `SUPERUSER`, `CREATEROLE`, `CREATEDB`,
`REPLICATION`, or `BYPASSRLS`, no protected-object/schema owner, and no role
with a forbidden effective protected-object privilege. PostgreSQL 16+ follows
the applicable membership option on each edge; PostgreSQL 15 treats every
reachable membership conservatively as exercisable.

PostgreSQL 16+ `ADMIN TRUE` is evaluated as an escalation edge even when that
same membership has `SET FALSE, INHERIT FALSE`: runtime must have no direct or
reachable administrative option that lets it grant or modify a path to any
denied role or authority. An intermediate role that runtime can administer and
that can administer or assume a denied role is likewise unsafe.

The runtime connection MUST establish
`session_user = current_user = <configured runtime role>`. Validation binds
both server-reported identities to the username sent by the client during the
PostgreSQL startup exchange; matching `current_user` and `session_user` values
alone are not proof of a direct login. A nested rollback-only `RESET SESSION
AUTHORIZATION` probe MUST still resolve to that configured startup username.
This rejects a privileged backend that impersonates the runtime role with
`SET SESSION AUTHORIZATION`; the suspect connection is discarded. Its
reachable graph also excludes predefined roles that confer database-wide writes, database
ownership/maintenance, server file or program access, backend/checkpoint
control, or subscription creation. The v1 denylist, when a name exists in the
connected PostgreSQL version, is `pg_database_owner`, `pg_read_all_data`,
`pg_write_all_data`, `pg_read_server_files`, `pg_write_server_files`,
`pg_execute_server_program`, `pg_signal_backend`, `pg_checkpoint`,
`pg_signal_autovacuum_worker`, `pg_maintain`, and `pg_create_subscription`.
Effective protected-object ACL
validation remains mandatory even when a powerful role is not predefined.

Validation also rejects effective runtime `EXECUTE` on any routine outside the
locked `pg_catalog`/`information_schema` initdb baseline that is `SECURITY
DEFINER`, uses an untrusted procedural language, or
is owned by a superuser, `pgpipe_safety_owner`, or another role able to own,
alter, truncate, or delegate authority over protected storage. Routine bodies
are not treated as analyzable policy; unsafe owner/language authority plus
runtime executability is sufficient to reject the path. The sole exception is
the definition-time-parsed, zero-input activator in section
4.8, and only its profile-locked direct ACL. Validation first resolves exactly
one OID from its schema, name, and empty input signature, then uses that OID
consistently to join its definition, owner, dependency, configuration, comment,
and ACL records. A name or schema match by itself is never sufficient.

For `pg_catalog` and `information_schema` routines, runtime may rely only on the
PostgreSQL-version's initdb privilege baseline. Validation compares current ACLs with
`pg_init_privs` (and the built-in default where no initial ACL row exists) and
rejects any effective `EXECUTE` acquired through a later direct, `PUBLIC`, or
membership grant on a routine that the baseline did not expose to `PUBLIC`.
It also rejects added or replaced routines in either namespace; the baseline
is an exact catalog identity, not a namespace wildcard.
This rule includes the server-file, large-object file, signaling, WAL,
checkpoint, backup, restore, and server-administration routines; pgpipe does
not maintain a fragile partial name list. It also rejects a changed owner,
language, `prosecdef`, or executable identity for a built-in routine used by
validation. The resulting code is `repair_safety_runtime_routine_acl_invalid`.

Contract v1 realizes that exact identity with reviewed, embedded PostgreSQL
major-version fingerprints over the complete `pg_catalog` and
`information_schema` `pg_proc` definitions plus `pg_aggregate` metadata. The
fingerprint includes OIDs and executable catalog fields such as `prosrc` and
`probin`; mutable routine ACLs are excluded only because they are validated
separately against `pg_init_privs`. A live destination can match one of the
reviewed count-and-digest tuples for its major version, but bootstrap never
learns or blesses a baseline from that destination. Separate reviewed ARM64
and AMD64 tuples account for PostgreSQL's signed/unsigned byte display in
serialized constant nodes; runtime validation still hashes the full raw
catalog without normalizing executable fields. Pinned official image
provenance and complete catalog regression fixtures are recorded under
`internal/sink/postgres/testdata/system-routines/`. A legitimate PostgreSQL
minor release or build that changes this initialized catalog requires an
explicitly reviewed additional tuple and fails closed until pgpipe ships it.

At database scope, the runtime role has effective `CONNECT, TEMPORARY` and has
neither database ownership nor `CREATE`. `TEMPORARY` is required by the hybrid
writer's `pg_temp` staging table and is harmless to the fully qualified safety
schema boundary. A grant through `PUBLIC` or a membership counts as effective
authority; unrelated application roles may retain their independently
authorized database privileges. Runtime and every reachable role have no grant
option on a database privilege. Missing `CONNECT`/`TEMPORARY`, runtime
database ownership, or effective database `CREATE` reports
`repair_safety_database_acl_invalid`.

Greenfield configuration accepts only an empty
`destination.write.session_replication_role` (the default), explicit `origin`,
or `replica`; it rejects `local`. Runtime receives **no** `SET` privilege on
`session_replication_role` in any mode. PostgreSQL parameter ACLs are
cluster-wide, so granting that privilege would let a reused login suppress
triggers and foreign keys in another connectable database. Contract v1 avoids
that blast radius rather than trying to repair unrelated database `CONNECT`
ACLs.

Empty and explicit `origin` are canonicalized to the same behavior: no
applicable role/database default sets `session_replication_role`, pgpipe issues
no `SET`, and every new runtime session must report effective `origin`.

Both `origin` and `replica` are supported on every contract-v1 PostgreSQL
major. In `replica`, every destination session must
still start and validate as `origin`. A new dedicated pinned apply connection
then calls the fully qualified, zero-argument protected activator exactly once
in its own successful autocommit statement, before preparing statements or
performing any customer or safety work, and verifies effective `replica` after
that statement commits. Activation inside a work transaction is forbidden
because a later rollback could undo the session-level change. The activator is
database-local, accepts no input, and is the only path from runtime to the
owner's cluster-wide parameter capability.

Activated physical connections belong to a pipeline/profile-dedicated pool and
carry a process-local lifecycle/generation tag. They are never returned to a
generic pool or a different database, pipeline, runtime role, or origin
profile. An untagged/new physical connection must begin at `origin` and activate
afresh. Driver `DISCARD ALL`/reset hooks are disabled for activated connections;
if the driver reports a reset, reconnect, backend change, or generation loss,
that physical connection is removed from service and must complete the bounded
origin/activate/post-commit-verify handshake again before reuse. `RESET ALL` or
`DISCARD ALL` may legitimately restore the stricter baseline `origin` even
though direct `SET/RESET session_replication_role` is denied; pgpipe never
issues them on an active pinned connection and treats a later unexpected mode
as a close/fail condition. Logical checkout and ordinary transaction/commit do
not query or reactivate the unchanged physical session. The activation adds no
per-event, per-row, per-source-transaction, or ordinary destination-commit
work.

Runtime has no effective `ALTER SYSTEM` on any parameter and no direct,
`PUBLIC`, or reachable-membership `pg_parameter_acl` entry for
`session_replication_role` or any other superuser-restricted parameter. Ordinary
user-settable parameters retain their PostgreSQL-defined behavior without
explicit ACL entries. Every supported major performs the exact
`pg_parameter_acl` checks, including the owner's one exact direct `SET` grant.
No
`pg_db_role_setting` row applicable to runtime may set
`session_replication_role` in any profile. Initialization, startup, the watcher,
diagnostics, every repair admission/claim, and every new pooled/pinned runtime
connection validate the applicable defaults, activator definition/ACL,
cluster-wide parameter ACL, and live effective value before use. A later grant,
default, function, or function-ACL change fails with
`repair_safety_runtime_parameter_acl_invalid`; watcher severity follows the
core-safety cancellation rule. Tests prove that runtime cannot directly
`SET`/`RESET` the session value or `ALTER ROLE ... SET/RESET` its
defaults, that reset/discard and pool reconnect hooks cannot silently return an
origin session as replica-ready, that the activator is absent outside an
initialized destination database, and that
global/role/database `replica` or `local` defaults are rejected because every
untagged runtime session must start at `origin`.

The runtime role identity and selected `origin`/`replica` profile are
lifecycle-bearing and immutable after the fresh installation is published.
Selecting a different runtime role or profile requires a new greenfield pgpipe
control/safety installation; v1 provides no role/profile-change migration. A
configured value that differs from the stored singleton profile fails with the
more specific `repair_safety_replication_profile_mismatch` before connection
activation.

### 3.3 Protected schema

`pgpipe_safety` is owned by `pgpipe_safety_owner` and carries the exact comment:

```text
pgpipe protected destination safety schema v1
```

The runtime role receives `USAGE` and never `CREATE`. `PUBLIC` receives no
privilege. The schema contains exactly the seven protected tables and their
allowlisted dependent constraints and indexes described below, plus the one
exact activator. Version 1 has no independently created sequences, other functions,
procedures, views, materialized views, user triggers, or custom types.
PostgreSQL-generated table row
types, array types, TOAST relations/indexes, and the internal RI triggers for
the one allowlisted foreign key are catalog auxiliaries, not extra shipped
objects.

### 3.4 Runtime schema

`pgpipe_runtime` is owned by the configured runtime role and carries the exact
comment:

```text
pgpipe runtime-managed destination objects v1
```

The runtime role receives `USAGE, CREATE`; `PUBLIC` receives no privilege.
Operational heartbeat, temporary, and staging objects that do not need to live
beside an application table belong here. Rebuild shadows that must atomically
replace an application table remain in that mapped application schema; they
are not protected safety metadata.

Each configured pipeline owns a deterministic operational heartbeat relation
named `heartbeat_<pipeline-hash>` in this schema. The hash is derived from the
logical pipeline name, keeps the identifier within PostgreSQL's length limit,
and prevents one pipeline from satisfying another pipeline's liveness check
when both share a destination database.

## 4. Protected object manifest

All seven relations are permanent ordinary heap tables in `pgpipe_safety`.
Column order, type, nullability, defaults, constraints, indexes, comments, and
dependencies are exact contract data. Extra or missing elements fail
validation. Constraint expressions are compared using parsed catalog identity
and normalized PostgreSQL output, not unsafe substring matching.

Unless a line below says otherwise, every primary-key or unique constraint is
immediate, `NOT DEFERRABLE`, validated, and backed by a valid/ready immediate
unique B-tree index. Every listed ordinary/partial index is a valid/ready B-tree
index in the database default tablespace with no `INCLUDE` columns, expression
keys, storage parameters, or nondefault operator classes; keys are ascending
with `NULLS LAST`, and text keys use the column's `pg_catalog."C"` collation.
Indexes are live, not clustered, not exclusion indexes, not replica identity,
and use ordinary NULL-distinct uniqueness. Every manifest constraint is local,
validated, non-inherited, has no parent, and is neither a period nor overlap
constraint; CHECK constraints use the default inheritable form. All tables
have empty relation options, no access-method override beyond heap, and no
replica-identity override. These catalog properties are part of exact
version-aware validation.

PostgreSQL 18 represents `NOT NULL` declarations as
`pg_constraint.contype = 'n'` rows and expose `conenforced`. Bootstrap therefore
uses a version-aware manifest:

- on PostgreSQL 15-17, every column marked `NOT NULL` below has
  `pg_attribute.attnotnull = true` and there is no separate not-null
  `pg_constraint` row;
- on PostgreSQL 18, each such column additionally has one explicitly named
  constraint `pgpipe_safety_nn_<table-ordinal>_<column-ordinal>_v1`, where table
  ordinals are sections 4.1 through 4.7 and column ordinals are the 1-based
  rows in that table's column list. It has `contype = 'n'`, a one-element
  `conkey` for that column, `convalidated = true`, `conenforced = true`,
  `conislocal = true`, `coninhcount = 0`, `connoinherit = false`,
  `conparentid = 0`, `contypid = 0`, and no supporting/referenced index or
  relation;
- on PostgreSQL 18, every CHECK, primary-key, unique, foreign-key, and
  not-null constraint in this manifest has `conenforced = true`; any
  `NOT ENFORCED` object is a definition mismatch. All period/overlap fields are
  false. On PostgreSQL 15-17 the validator does not query catalog columns or
  constraint types that do not exist.

The version-generated not-null rows are the only additional constraint rows
allowed. Their deterministic names and attributes are exact contract data, not
a wildcard for other generated constraints.

Contract v1 supports PostgreSQL majors 15 through 18. PostgreSQL 14 and any
newer, unreviewed major fail with
`repair_bootstrap_postgres_version_unsupported` during initialization or
`repair_safety_postgres_version_unsupported` during configuration/startup
validation until its catalog behavior has a reviewed contract revision;
pgpipe never guesses that an unknown catalog shape is safe.

Every `text` and `text[]` column in this manifest has explicit collation
`pg_catalog."C"`; validation compares `pg_attribute.attcollation` with that
exact built-in collation OID. Consequently every text key/index and every
regular-expression/check comparison uses deterministic byte-oriented C
semantics. Each regex expression below explicitly applies
`COLLATE pg_catalog."C"` to its input as defense in depth. Destination/database
default collation never changes the accepted ASCII ID, decimal, or digest
language.

### 4.1 `destination_identity`

Exact table comment: `pgpipe protected destination identity v1`.

| Column | Type | Required/default |
| --- | --- | --- |
| `singleton` | `smallint` | `NOT NULL` |
| `destination_id` | `text` | `NOT NULL` |
| `replication_profile` | `text` | `NOT NULL` |
| `created_at` | `timestamptz` | `NOT NULL` |

Required constraints/indexes:

- `destination_identity_pkey`: primary key (`singleton`);
- `destination_identity_singleton_check`: `singleton = 1`;
- `destination_identity_id_check`:
  `destination_id COLLATE pg_catalog."C" ~ '^dst-[0-9a-f]{32}$'`;
- `destination_identity_replication_profile_check`:
  `replication_profile IN ('origin', 'replica')`;
- the primary-key index only.

It contains exactly one row. `destination_id` is a canonical lowercase
`dst-` identifier with 32 hexadecimal characters. Bootstrap generates it from
16 bytes produced by the operating system CSPRNG, binds `created_at` explicitly,
journals the canonical selected replication profile, and inserts the singleton
once in the initialization transaction. The same
journaled attempt reuses its already committed value during reconciliation; no
later startup, retry, or runtime path updates/regenerates it. Runtime has
read-only access. A missing, second, changed, noncanonical, or journal-mismatched
identity is `repair_safety_destination_identity_invalid`, never an invitation
to reseed it. A configured profile that differs from the stored profile is
`repair_safety_replication_profile_mismatch` and requires a fresh greenfield
installation.

### 4.2 `pipeline_fence`

Exact table comment: `pgpipe protected pipeline ownership fence v1`.

| Column | Type | Required/default |
| --- | --- | --- |
| `pipeline_id` | `text` | `NOT NULL` |
| `pipeline_name` | `text` | `NOT NULL` |
| `lineage_id` | `text` | `NOT NULL` |
| `activation_id` | `text` | `NOT NULL` |
| `previous_generation` | `bigint` | `NOT NULL` |
| `owner_id` | `text` | `NOT NULL` |
| `generation` | `bigint` | `NOT NULL` |
| `checkpoint_id` | `text` | `NOT NULL` |
| `previous_checkpoint_id` | `text` | `NOT NULL` |
| `activated_at` | `timestamptz` | `NOT NULL` |

Required constraints/indexes:

- `pipeline_fence_pkey`: primary key (`pipeline_id`);
- `pipeline_fence_pipeline_id_check`:
  `octet_length(pipeline_id) BETWEEN 1 AND 256`;
- `pipeline_fence_pipeline_name_check`:
  `octet_length(pipeline_name) BETWEEN 1 AND 256 AND pipeline_name = btrim(pipeline_name)`;
- `pipeline_fence_lineage_id_check`:
  `lineage_id COLLATE pg_catalog."C" ~ '^lin-[0-9a-f]{32}$'`;
- `pipeline_fence_activation_id_check`:
  `activation_id COLLATE pg_catalog."C" ~ '^act-[0-9a-f]{32}$'`;
- `pipeline_fence_previous_generation_check`: `previous_generation >= 0`;
- `pipeline_fence_owner_id_check`:
  `owner_id = 'offline-rebuild' OR owner_id COLLATE pg_catalog."C" ~
  '^own-[0-9a-f]{32}$'`;
- `pipeline_fence_generation_check`:
  `generation > 0 AND previous_generation <= generation`;
- `pipeline_fence_checkpoint_id_check`:
  `checkpoint_id COLLATE pg_catalog."C" ~ '^chk-[0-9a-f]{32}$'`;
- `pipeline_fence_previous_checkpoint_id_check`:
  `previous_checkpoint_id COLLATE pg_catalog."C" ~ '^chk-[0-9a-f]{32}$'`;
- the primary-key index only.

### 4.3 `pipeline_fence_audit`

Exact table comment: `pgpipe protected pipeline ownership fence audit v1`.

| Column | Type | Required/default |
| --- | --- | --- |
| `request_id` | `text` | `NOT NULL` |
| `pipeline_id` | `text` | `NOT NULL` |
| `pipeline_name` | `text` | `NOT NULL` |
| `action` | `text` | `NOT NULL` |
| `previous_lineage_id` | `text` | nullable |
| `previous_generation` | `bigint` | nullable |
| `lineage_id` | `text` | `NOT NULL` |
| `activation_id` | `text` | `NOT NULL` |
| `destination_id` | `text` | `NOT NULL` |
| `generation` | `bigint` | `NOT NULL` |
| `checkpoint_id` | `text` | `NOT NULL` |
| `previous_checkpoint_id` | `text` | `NOT NULL` |
| `config_evidence` | `text` | `NOT NULL` |
| `destination_evidence` | `text` | `NOT NULL` |
| `operator_identity` | `text` | `NOT NULL` |
| `reason` | `text` | `NOT NULL` |
| `recorded_at` | `timestamptz` | `NOT NULL DEFAULT now()` |

Required constraints/indexes:

- `pipeline_fence_audit_pkey`: primary key (`request_id`);
- `pipeline_fence_audit_request_id_check`:
  `octet_length(request_id) BETWEEN 1 AND 128 AND request_id = btrim(request_id)`;
- `pipeline_fence_audit_pipeline_id_check`:
  `octet_length(pipeline_id) BETWEEN 1 AND 256`;
- `pipeline_fence_audit_pipeline_name_check`:
  `octet_length(pipeline_name) BETWEEN 1 AND 256 AND pipeline_name = btrim(pipeline_name)`;
- `pipeline_fence_audit_action_check`:
  `action IN ('initialize', 'authorize_rebuild')`;
- `pipeline_fence_audit_previous_check`: `previous_lineage_id` and
  `previous_generation` satisfy `(previous_lineage_id IS NULL AND
  previous_generation IS NULL) OR (previous_lineage_id IS NOT NULL AND
  previous_lineage_id COLLATE pg_catalog."C" ~ '^lin-[0-9a-f]{32}$' AND
  previous_generation IS NOT NULL AND previous_generation >= 0)`;
- `pipeline_fence_audit_lineage_id_check`:
  `lineage_id COLLATE pg_catalog."C" ~ '^lin-[0-9a-f]{32}$'`;
- `pipeline_fence_audit_activation_id_check`:
  `activation_id COLLATE pg_catalog."C" ~ '^act-[0-9a-f]{32}$'`;
- `pipeline_fence_audit_destination_id_check`:
  `destination_id COLLATE pg_catalog."C" ~ '^dst-[0-9a-f]{32}$'`;
- `pipeline_fence_audit_generation_check`: `generation > 0`;
- `pipeline_fence_audit_checkpoint_id_check`:
  `checkpoint_id COLLATE pg_catalog."C" ~ '^chk-[0-9a-f]{32}$'`;
- `pipeline_fence_audit_previous_checkpoint_id_check`:
  `previous_checkpoint_id COLLATE pg_catalog."C" ~ '^chk-[0-9a-f]{32}$'`;
- `pipeline_fence_audit_config_evidence_check`:
  `config_evidence COLLATE pg_catalog."C" ~ '^[0-9a-f]{64}$'`;
- `pipeline_fence_audit_destination_evidence_check`:
  `destination_evidence COLLATE pg_catalog."C" ~ '^[0-9a-f]{64}$'`;
- `pipeline_fence_audit_operator_check`:
  `octet_length(operator_identity) BETWEEN 1 AND 256 AND operator_identity =
  btrim(operator_identity) AND operator_identity COLLATE pg_catalog."C" !~
  '[[:cntrl:]]'`;
- `pipeline_fence_audit_reason_check`:
  `octet_length(reason) BETWEEN 10 AND 500 AND reason = btrim(reason) AND
  reason COLLATE pg_catalog."C" !~ '[[:cntrl:]]'`;
- the primary-key index only.

There is deliberately no foreign key to `pipeline_fence`: audit history must
survive a missing or rebuilt active row.

`initialize` writes the same canonical activation ID to the fence and audit
row and writes a cryptographically random 16-byte lowercase-hex live process
owner ID (`own-` plus 32 characters) to the fence. An offline
`authorize_rebuild` request generates one cryptographically random 16-byte
lowercase-hex activation ID (`act-` plus 32 characters), persists it with the
exact request proof, and atomically writes that same value to the fence and
audit row. Its fence `owner_id` is the exact nonempty sentinel
`offline-rebuild`; it never uses an empty owner or an `admin-` activation
prefix.

The next live acquisition recognizes only that sentinel, then requires the
pipeline-scoped state authorization and audit row to match request ID, action,
lineage, activation, generation, destination identity, checkpoints, evidence
hashes, operator, and reason before replacing the sentinel with a normal live
owner/activation transition. Missing or different evidence blocks startup as
recovery-required. Contract v1 has no `adopt_legacy` action or compatibility
recognition of the old `admin-<request>` convention.

### 4.4 `apply_ledger`

Exact table comment: `pgpipe protected apply ledger v1`.

| Column | Type | Required/default |
| --- | --- | --- |
| `pipeline_name` | `text` | `NOT NULL` |
| `source_commit_lsn` | `text` | `NOT NULL` |
| `source_commit_lsn_pg` | `pg_lsn` | `NOT NULL` |
| `source_xid` | `bigint` | `NOT NULL` |
| `source_commit_timestamp` | `timestamptz` | nullable |
| `event_count` | `bigint` | `NOT NULL` |
| `first_applied_at` | `timestamptz` | `NOT NULL DEFAULT now()` |
| `last_seen_at` | `timestamptz` | `NOT NULL DEFAULT now()` |
| `replay_count` | `bigint` | `NOT NULL DEFAULT 0` |
| `status` | `text` | `NOT NULL DEFAULT 'applied'` |
| `skipped_at` | `timestamptz` | nullable |
| `skipped_by` | `text` | nullable |
| `skipped_reason` | `text` | nullable |

Required constraints/indexes:

- `apply_ledger_pkey`: primary key
  (`pipeline_name`, `source_commit_lsn`, `source_xid`);
- `apply_ledger_status_check`: `status IN ('applied', 'skipped')`;
- `apply_ledger_lsn_consistency_check`:
  `source_commit_lsn = source_commit_lsn_pg::text`;
- `apply_ledger_lsn_positive_check`:
  `source_commit_lsn_pg > '0/0'::pg_catalog.pg_lsn`;
- `apply_ledger_source_xid_check`:
  `source_xid BETWEEN 1 AND 4294967295`;
- `apply_ledger_event_count_check`: `event_count >= 0`;
- `apply_ledger_replay_count_check`: `replay_count >= 0`;
- `apply_ledger_skip_evidence_check`:
  `(status = 'applied' AND skipped_at IS NULL AND skipped_by IS NULL AND
  skipped_reason IS NULL) OR (status = 'skipped' AND skipped_at IS NOT NULL AND
  skipped_by IS NOT NULL AND octet_length(skipped_by) BETWEEN 1 AND 256 AND
  skipped_by = btrim(skipped_by) AND skipped_by COLLATE pg_catalog."C" !~
  '[[:cntrl:]]' AND skipped_reason IS NOT NULL AND
  octet_length(skipped_reason) BETWEEN 10 AND 500 AND skipped_reason =
  btrim(skipped_reason) AND skipped_reason COLLATE pg_catalog."C" !~
  '[[:cntrl:]]')`;
- `apply_ledger_pipeline_lsn_idx`: unique
  (`pipeline_name`, `source_commit_lsn`);
- `apply_ledger_pipeline_lsn_pg_idx`: non-unique
  (`pipeline_name`, `source_commit_lsn_pg`).

### 4.5 `table_rebuild_ledger`

Exact table comment: `pgpipe protected table rebuild ledger v1`.

| Column | Type | Required/default |
| --- | --- | --- |
| `job_id` | `text` | `NOT NULL` |
| `pipeline_name` | `text` | `NOT NULL` |
| `proof_hash` | `text` | `NOT NULL` |
| `source_schema` | `text` | `NOT NULL` |
| `source_table` | `text` | `NOT NULL` |
| `source_table_oid` | `bigint` | `NOT NULL` |
| `destination_schema` | `text` | `NOT NULL` |
| `destination_table` | `text` | `NOT NULL` |
| `destination_table_oid` | `bigint` | `NOT NULL` |
| `source_shape_hash` | `text` | `NOT NULL` |
| `column_names` | `text[]` | `NOT NULL` |
| `snapshot_lsn` | `pg_lsn` | `NOT NULL` |
| `phase` | `text` | `NOT NULL` |
| `rows_copied` | `bigint` | `NOT NULL DEFAULT 0` |
| `copy_batches` | `bigint` | `NOT NULL DEFAULT 0` |
| `sequences_resynced` | `integer` | `NOT NULL DEFAULT 0` |
| `failure_message` | `text` | `NOT NULL DEFAULT ''` |
| `created_at` | `timestamptz` | `NOT NULL DEFAULT now()` |
| `reset_at` | `timestamptz` | `NOT NULL` |
| `updated_at` | `timestamptz` | `NOT NULL` |
| `completed_at` | `timestamptz` | nullable |
| `failed_at` | `timestamptz` | nullable |

Required constraints/indexes:

- `table_rebuild_ledger_pkey`: primary key (`job_id`);
- `table_rebuild_ledger_phase_check`:
  `phase IN ('reset', 'copying', 'sequences_resynced', 'completed', 'failed',
  'abandoned')`;
- `table_rebuild_ledger_rows_copied_check`: `rows_copied >= 0`;
- `table_rebuild_ledger_copy_batches_check`: `copy_batches >= 0`;
- `table_rebuild_ledger_sequences_resynced_check`:
  `sequences_resynced >= 0`;
- `table_rebuild_ledger_proof_hash_check`:
  `proof_hash COLLATE pg_catalog."C" ~ '^sha256:[0-9a-f]{64}$'`;
- `table_rebuild_ledger_shape_hash_check`:
  `source_shape_hash COLLATE pg_catalog."C" ~ '^sha256:[0-9a-f]{64}$'`;
- `table_rebuild_ledger_source_oid_check`:
  `source_table_oid BETWEEN 1 AND 4294967295`;
- `table_rebuild_ledger_destination_oid_check`:
  `destination_table_oid BETWEEN 1 AND 4294967295`;
- `table_rebuild_one_active_destination_idx_v1`: unique
  (`destination_schema`, `destination_table`) with exact predicate
  `WHERE phase IN ('reset', 'copying', 'sequences_resynced')`;
- exact index comment:
  `pgpipe protected active table rebuild destination guard v1`.

### 4.6 `repair_execution_fence`

Exact table comment: `pgpipe protected repair execution fence v1`.

The exact columns remain the repair-approval v2 evidence model:

```text
pipeline_id text NOT NULL
repair_job_id text NOT NULL
execution_claim_id text NOT NULL
ownership_generation bigint NOT NULL
source_system_identifier text NOT NULL
source_database_oid bigint NOT NULL
source_relation_oid bigint NOT NULL
destination_database_identity text NOT NULL
destination_relation_oid bigint NOT NULL
workflow_version smallint NOT NULL
plan_hash text NOT NULL
mapping_transform_fingerprint text NOT NULL
phase text NOT NULL
cleanup_state text NOT NULL
guard_state text NOT NULL
created_at timestamptz NOT NULL
started_at timestamptz NULL
completed_at timestamptz NULL
cleanup_completed_at timestamptz NULL
guard_released_at timestamptz NULL
```

Required constraints are named exactly:

- `repair_execution_fence_pkey`: primary key
  (`pipeline_id`, `repair_job_id`, `execution_claim_id`);
- `repair_execution_fence_pipeline_job_key`: unique
  (`pipeline_id`, `repair_job_id`);
- `repair_execution_fence_chunk_binding_key`: unique
  (`pipeline_id`, `repair_job_id`, `execution_claim_id`,
  `ownership_generation`, `destination_database_identity`,
  `destination_relation_oid`);
- `repair_execution_fence_pipeline_id_check`:
  `octet_length(pipeline_id) BETWEEN 1 AND 256`;
- `repair_execution_fence_job_id_check`:
  `octet_length(repair_job_id) BETWEEN 1 AND 256`;
- `repair_execution_fence_claim_id_check`:
  `octet_length(execution_claim_id) BETWEEN 1 AND 256`;
- `repair_execution_fence_generation_check`: `ownership_generation > 0`;
- `repair_execution_fence_source_system_check`: canonical unsigned 64-bit
  decimal text, expressed as `source_system_identifier COLLATE
  pg_catalog."C" ~ '^[1-9][0-9]{0,19}$' AND
  (pg_catalog.octet_length(source_system_identifier) < 20 OR
  source_system_identifier COLLATE pg_catalog."C" <=
  '18446744073709551615')`;
- `repair_execution_fence_source_database_oid_check`:
  `source_database_oid BETWEEN 1 AND 4294967295`;
- `repair_execution_fence_source_relation_oid_check`:
  `source_relation_oid BETWEEN 1 AND 4294967295`;
- `repair_execution_fence_destination_identity_check`:
  `destination_database_identity COLLATE pg_catalog."C" ~
  '^dst-[0-9a-f]{32}$'`;
- `repair_execution_fence_destination_relation_oid_check`:
  `destination_relation_oid BETWEEN 1 AND 4294967295`;
- `repair_execution_fence_workflow_version_check`: `workflow_version = 2`;
- `repair_execution_fence_plan_hash_check`:
  `plan_hash COLLATE pg_catalog."C" ~ '^sha256:[0-9a-f]{64}$'`;
- `repair_execution_fence_mapping_hash_check`:
  `mapping_transform_fingerprint COLLATE pg_catalog."C" ~
  '^sha256:[0-9a-f]{64}$'`;
- `repair_execution_fence_phase_check`:
  `phase IN ('claimed', 'cutover_captured', 'copying', 'replaying',
  'verifying', 'completed', 'interrupted_prewrite', 'interrupted_partial',
  'interrupted_ambiguous', 'interrupted_postwrite')`;
- `repair_execution_fence_cleanup_check`:
  `cleanup_state IN ('not_required', 'pending', 'complete', 'failed')`;
- `repair_execution_fence_guard_check`:
  `guard_state IN ('active', 'cleanup_pending', 'recovery_required',
  'released')`.

Required indexes:

- indexes backing the primary and unique constraints;
- `repair_execution_one_active_destination_idx_v1`: unique
  (`destination_database_identity`, `destination_relation_oid`) where
  exact predicate `WHERE guard_state <> 'released'`;
- exact partial-index comment:
  `pgpipe protected repair destination guard v1`.

### 4.7 `repair_chunk_ledger`

Exact table comment: `pgpipe protected repair chunk ledger v1`.

```text
pipeline_id text NOT NULL
repair_job_id text NOT NULL
execution_claim_id text NOT NULL
chunk_sequence bigint NOT NULL
ownership_generation bigint NOT NULL
destination_database_identity text NOT NULL
destination_relation_oid bigint NOT NULL
action_proof_version smallint NOT NULL
action_hash text NOT NULL
insert_count bigint NOT NULL
update_count bigint NOT NULL
delete_count bigint NOT NULL
chunk_bytes bigint NOT NULL
committed_at timestamptz NOT NULL
```

Required constraints are named exactly:

- `repair_chunk_ledger_pkey`: primary key
  (`pipeline_id`, `repair_job_id`, `execution_claim_id`, `chunk_sequence`);
- `repair_chunk_ledger_logical_sequence_key`: unique
  (`pipeline_id`, `repair_job_id`, `chunk_sequence`);
- `repair_chunk_ledger_execution_fkey`: the only protected-table foreign key,
  from (`pipeline_id`, `repair_job_id`, `execution_claim_id`,
  `ownership_generation`, `destination_database_identity`,
  `destination_relation_oid`) to the matching columns of
  `repair_execution_fence_chunk_binding_key`, `MATCH SIMPLE`,
  `ON UPDATE NO ACTION`, `ON DELETE RESTRICT`, not deferrable and validated;
- `repair_chunk_ledger_sequence_check`: `chunk_sequence > 0`;
- `repair_chunk_ledger_generation_check`: `ownership_generation > 0`;
- `repair_chunk_ledger_destination_identity_check`:
  `destination_database_identity COLLATE pg_catalog."C" ~
  '^dst-[0-9a-f]{32}$'`;
- `repair_chunk_ledger_destination_relation_oid_check`:
  `destination_relation_oid BETWEEN 1 AND 4294967295`;
- `repair_chunk_ledger_proof_version_check`: `action_proof_version = 1`;
- `repair_chunk_ledger_action_hash_check`:
  `action_hash COLLATE pg_catalog."C" ~ '^sha256:[0-9a-f]{64}$'`;
- `repair_chunk_ledger_insert_count_check`,
  `repair_chunk_ledger_update_count_check`, and
  `repair_chunk_ledger_delete_count_check`: the respective count is
  nonnegative;
- `repair_chunk_ledger_action_count_check`:
  `insert_count > 0 OR update_count > 0 OR delete_count > 0`;
- `repair_chunk_ledger_bytes_check`: `chunk_bytes > 0`.

In addition to the two constraint-owned unique indexes, the chunk table has
`repair_chunk_ledger_execution_idx_v1`, a non-unique index on
(`pipeline_id`, `repair_job_id`, `execution_claim_id`,
`ownership_generation`, `destination_database_identity`,
`destination_relation_oid`). This is the exact foreign-key lookup index; the
parent binding key has its own constraint-owned unique index.

PostgreSQL creates the ordinary internal RI triggers for
`repair_chunk_ledger_execution_fkey`; bootstrap MUST NOT alter their enablement
except as specified here. Bootstrap marks every trigger attached to that exact
foreign key `ENABLE ALWAYS`. Validation joins each internal trigger to that
exact constraint OID, requires `tgenabled = 'A'`, and does not treat
PostgreSQL-generated trigger names as stable. No other user or internal trigger
is allowlisted. This preserves the protected foreign key after an apply
connection activates `replica`.

The repair writer receives only a physical connection that completed the
profile-specific connection-generation handshake and never issues `SET`,
`RESET`, or `DISCARD`. In a repair-chunk transaction, customer actions
and the chunk-ledger insert therefore use the same connection-level mode. The
foreign-key triggers use their locked `ENABLE ALWAYS` mode, are immediate and
not deferrable, and therefore complete their check before commit in either
`origin` or activated `replica`. This repair-only
protocol does not add work to ordinary streaming
commits. A runtime actor deliberately bypassing the allowed DML protocol remains
in the documented compromised-runtime boundary; fencing, CAS, reconciliation,
and negative tests are still required.

### 4.8 Replica-session activator

Every contract-v1 installation contains exactly one protected routine:
`pgpipe_safety.activate_replica_session_v1()`. Its exact comment is
`pgpipe protected replica session activator v1`
and its definition is:

```sql
CREATE FUNCTION pgpipe_safety.activate_replica_session_v1()
RETURNS pg_catalog.text
LANGUAGE sql
VOLATILE
PARALLEL UNSAFE
SECURITY DEFINER
SET search_path = pg_catalog, pg_temp
BEGIN ATOMIC
  SELECT pg_catalog.set_config(
    'session_replication_role',
    'replica',
    false
  );
END;
```

Catalog identity is exact: it is a zero-argument ordinary function owned by
`pgpipe_safety_owner`, returns one non-set `pg_catalog.text`, is called on null
input, is non-leakproof, volatile, parallel-unsafe, and has only the exact
`search_path=pg_catalog, pg_temp` `proconfig` entry. It uses the built-in SQL
language and a definition-time parsed SQL body whose sole executable call is
the fully qualified built-in three-argument `pg_catalog.set_config(text, text,
boolean)` with the three fixed literals above. It has no argument/default,
variadic/out parameter, relation reference, support function, transform,
dynamic SQL, or configuration/input-dependent branch. The normal built-in SQL
function cost and non-set row estimate are exact for each supported major.
Validation inspects the stored `prosqlbody` expression tree and requires exactly
one function-expression node with the separately validated built-in
`set_config` OID. PostgreSQL does not record `pg_depend` edges to pinned
built-ins, so the activator MUST have zero recorded `pg_proc` dependencies; any
such row is unexpected and fails validation.

`PUBLIC` never has `EXECUTE`. The configured runtime role receives direct
`EXECUTE` without grant option only when the stored lifecycle profile is
`replica`; in `origin` it receives none. No other ordinary role has direct or
effective execution. Incoming runtime memberships are forbidden as specified
above, so the grant cannot be inherited by another login. Tests call the
routine directly as runtime in both profiles, prove denial in `origin`, prove a
successful autocommit activation and post-commit `replica`,
prove rollback does not masquerade as activation, and prove the routine is
absent in an unrelated database.

The schema, seven tables, two named partial indexes, and exact activator
carry only the exact comments stated in this contract. Protected columns,
constraints, primary-key indexes, constraint-owned unique indexes, and every
other protected catalog object have no comment. An absent, additional, or
changed marker is a definition mismatch.

## 5. Exact runtime ACL

`pgpipe_writer` below means the actual configured destination runtime role.

| Protected object | Exact direct runtime privileges |
| --- | --- |
| schema `pgpipe_safety` | `USAGE` |
| `destination_identity` | `SELECT` |
| `pipeline_fence` | `SELECT, INSERT, UPDATE` |
| `pipeline_fence_audit` | `SELECT, INSERT` |
| `apply_ledger` | `SELECT, INSERT, UPDATE, DELETE` |
| `table_rebuild_ledger` | `SELECT, INSERT, UPDATE` |
| `repair_execution_fence` | `SELECT, INSERT, UPDATE` |
| `repair_chunk_ledger` | `SELECT, INSERT` |
| `activate_replica_session_v1()` | `EXECUTE` only for stored `replica`; none for stored `origin` |

Each protected table also creates one PostgreSQL composite row type and its
generated array type. Bootstrap executes `REVOKE USAGE ON TYPE
pgpipe_safety.<table> FROM PUBLIC` for every row type. Each row type has a
non-null normalized ACL representing only the owner's type `USAGE`; by
ownership the owner retains grant authority. `PUBLIC`, runtime, and every other
ordinary role have no direct or effective `USAGE`. Each generated array type
has no independent explicit ACL (`typacl IS NULL`) and PostgreSQL's array-type
privilege handling is validated to delegate to that protected element row type.
An explicit array-type grant or any different row-type ACL is excessive. These
fourteen generated types remain auxiliary dependents, but their ACL state is
part of the exact manifest.

The ACL is exact, not a minimum. Validation rejects:

- `CREATE` on `pgpipe_safety`;
- table `TRUNCATE`, `REFERENCES`, or `TRIGGER`;
- any extra table DML privilege;
- column-level grants;
- grant options;
- grants to `PUBLIC` or an unapproved ordinary role;
- an activator `EXECUTE` grant that does not exactly match the stored profile;
- ownership by the runtime role;
- any `pg_default_acl` row for the protected owner in this destination database
  or for any role scoped to the `pgpipe_safety` namespace;
- a direct or indirect privilege path through role membership.

No protected object is granted with `ALL PRIVILEGES`. The owner retains
owner-level authority by PostgreSQL definition; the runtime receives only this
matrix.

Bootstrap never executes `ALTER DEFAULT PRIVILEGES`. The exact contract is
absence: this database has no `pg_default_acl` row whose owner is
`pgpipe_safety_owner` (global or schema-scoped), and no row for any role whose
namespace is `pgpipe_safety`. PostgreSQL's hard-wired defaults therefore remain
unchanged. The seven existing composite row types override the hard-wired
`PUBLIC USAGE` default explicitly as specified above, and the exact schema
manifest forbids every other future/unlisted object. A future contract
that adds an object type with a hard-wired `PUBLIC` privilege must lock and
apply that object's direct ACL explicitly rather than relying on a default-ACL
side effect. Every current runtime privilege is an explicit object grant from
the manifest. Validation checks both direct ACL entries and effective
privileges obtained through memberships; an exact direct ACL does not excuse
an excessive indirect privilege.

## 6. Dependency allowlist

The seven tables MUST have no dependency other than:

- `pg_catalog` scalar/array types listed in the manifest;
- exact `pg_catalog.now()` defaults;
- the exact built-in `pg_catalog."C"` collation dependency for every `text` and
  `text[]` attribute and for the listed constraint/index expression trees;
- exact `pg_catalog.octet_length(text)` and `pg_catalog.btrim(text)` calls in
  the listed CHECK expression trees;
- the exact built-in `pg_lsn`-to-`text` I/O-coercion expression used only by
  `apply_ledger_lsn_consistency_check`, resolving to the supported-version
  built-in `pg_catalog` type I/O identities and never a user-defined cast;
- the exact built-in unknown-literal-to-`pg_catalog.pg_lsn` coercion for
  `'0/0'::pg_catalog.pg_lsn` in `apply_ledger_lsn_positive_check`;
- built-in comparison, arithmetic, Boolean, regex, and null-test operators and
  default built-in operator classes that appear in the exact normalized
  constraint/index expression trees above;
- the one named foreign key from `repair_chunk_ledger` to
  `repair_execution_fence`;
- the exact ordinary internal RI triggers generated for that foreign key, with
  the version-locked enablement mode above;
- the one activator's dependencies on its protected
  schema/owner, built-in SQL language, `pg_catalog.text` return type, built-in
  `pg_catalog.set_config(text,text,boolean)`, and exact fixed search path;
- their own allowlisted constraints and indexes;
- their namespace and owner dependencies;
- PostgreSQL-generated composite row types, array types, and TOAST
  relations/indexes with exactly these internal chains: row type -> protected
  table; array type -> that row type -> protected table; TOAST relation ->
  protected table; TOAST index -> that TOAST relation -> protected table.

The following are forbidden:

- unlogged, temporary, foreign, partitioned, or inherited relations;
- table inheritance or partition attachments in either direction;
- row-level security, policies, user triggers, or rewrite rules;
- generated or identity columns;
- additional defaults, checks, unique constraints, indexes, or foreign keys;
- expression indexes or unlisted partial predicates;
- non-`pg_catalog` functions, operators, collations, or operator classes;
- owner-authorized dependent views, materialized views, functions, or
  `SECURITY DEFINER` routines other than the exact activator;
- dependencies from protected objects to `pgpipe_runtime` or an application
  schema;
- objects with invalid/not-ready indexes or unvalidated constraints.

Indexes, constraints, and internal RI triggers do not have independent
PostgreSQL owners. Their exact definitions and dependency identities are
validated as part of the owning protected table. Generated row/array types and
TOAST relations may carry the table owner's OID and are included in the
ownership closure only through those exact transitive dependency chains.
Version 1 defines no independently created sequence, custom type, or routine
other than the version-gated activator.

All safety SQL uses fully qualified identifiers. Any bootstrap or maintenance
transaction pins `search_path` to `pg_catalog, pg_temp` and never resolves an
unqualified executable object through a user-writable schema. Before DDL,
bootstrap also sets transaction-local `default_table_access_method = 'heap'`
and `default_tablespace = ''`; role/database/session defaults cannot change the
manifest access method or tablespace. The validator confirms the resulting
catalog identities.

## 7. Initialization and publication contract

### 7.1 One standard installation flow

The temporary `pgpipe init` wizard performs safety bootstrap as part of
**Finish installation**. Package scripts do not open database connections or
prompt for secrets. `pgpipe start` never opens setup mode and never performs
owner-level DDL.

The wizard collects the runtime destination settings first. The privileged
card reuses the server-held host, port, database, routing, and TLS settings and
accepts only:

- destination administrator username; and
- masked destination administrator password/token.

The browser cannot supply a second host, port, database, SSL mode, or runtime
role for the privileged connection.

The server clones the already parsed, immutable reviewed destination
connection configuration and assigns the DBA username and password as literal
driver credential fields. It MUST NOT interpolate either value into a DSN/URI,
reparse a credential-bearing connection string, or allow credential text to
change routing, TLS, options, `search_path`, or parameters. Username and
password reject U+0000/NUL; the password otherwise remains opaque and
unnormalized. Every dynamic SQL identifier, including the configured runtime
role, is emitted only through the driver's identifier-quoting primitive; every
data value is a bound parameter. Credential or identifier text is never SQL or
connection-string syntax.

The pending-config fingerprint is exactly `sha256:` plus lowercase hexadecimal
SHA-256 over the domain `pgpipe.destination-bootstrap.pending-config.v1` and a
binary length-delimited projection. Each item is encoded in the locked order as
`uint16-be(tag-byte-length) || UTF-8 tag || one-byte type ||
uint64-be(value-byte-length) || canonical value`; lists first encode their item
count and then each item with the same length rule. The projection contains the
config schema revision, destination contract ID/revision, reviewed routing mode,
ordered host/socket targets, port, database name, TLS mode/server name, SHA-256
fingerprints of reviewed CA/client public-certificate material, every parsed
non-secret connection option, configured runtime login/role, and configured
`session_replication_role` mode. Canonical integer, Boolean, byte, UTF-8 string,
and list encodings are distinct; unknown fields require a contract revision.
Plaintext/encoded database passwords, private keys, setup/bootstrap/idempotency
secrets, and every salted or unsalted password-derived value are excluded, so
the digest is not a credential oracle.

The server-held immutable parsed configuration is authoritative. A browser echo
of the fingerprint is only a binding assertion and can never select or alter a
field. Comparisons decode hexadecimal and use constant-time fixed 32-byte digest
comparison. Golden/adversarial tests prove that field order, tag, type, list
order, value, routing, TLS material, runtime identity, contract/config revision,
and domain changes produce a different digest while credential changes do not
enter the projection.

Reusing the reviewed destination target does not mean reusing an unsafe
transport policy. Before the browser accepts a DBA password, pgpipe MUST prove
that the server-to-database administrator connection uses TCP with
certificate-chain and hostname verification equivalent to libpq
`sslmode=verify-full` against the reviewed hostname and protected trust-root
files.

A TCP connection, including loopback, requires verify-full TLS. A
remote/plaintext connection, opportunistic encryption, `sslmode=disable`,
`allow`, `prefer`, `require`, or `verify-ca` is insufficient for a privileged
password. The wizard blocks before accepting/sending the credential and
returns `repair_bootstrap_database_transport_forbidden`. The operator must
correct the reviewed destination TLS settings; the browser cannot override
them for only the administrator. Unix-socket privileged bootstrap is not part
of contract v1 and returns the same transport-forbidden result. It may be added
only after an independent PostgreSQL service-identity trust anchor and peer
credential proof are designed and reviewed. The Docker demo prepares safety objects
inside the trusted disposable database initialization boundary and does not
send a DBA password through this browser/database path.

### 7.2 Same-destination proof

Before DDL, runtime and administrator connections MUST prove that they reach
the same live PostgreSQL database. Multi-host DSNs, transaction-routing pools,
and load-balanced bootstrap targets without pre-authentication database
affinity are rejected before the password field is enabled. For TCP, pgpipe
first opens the verify-full runtime connection, records its selected peer
address, and pins the administrator dial to that exact address/port while
retaining the original reviewed hostname for certificate and SNI validation.
The runtime session returns `current_database()` and its database OID and takes
a session-level advisory lock on a server-generated random two-part
key that is never exposed to the browser or logs. The administrator session
must report the same database name/OID and must fail to acquire that exact lock;
successful acquisition proves that it reached a different lock domain, so it
immediately releases the lock and bootstrap rejects the target. The runtime
session then releases its challenge lock. This live challenge needs no
`pg_stat_activity` visibility or extra monitoring role. Textual host/database
equality alone is not sufficient. Bootstrap requires session-stable database
connections; a transaction-pooling endpoint that cannot preserve this proof
is rejected with remediation to use a direct/session-mode initialization
endpoint. A mismatch returns `repair_bootstrap_target_mismatch` before any DDL.
The administrator connection is likewise bound to the username sent in its
PostgreSQL startup exchange: `current_user`, `session_user`, and a nested
rollback-only `RESET SESSION AUTHORIZATION` proof must all resolve to that
same direct login. `SET ROLE` and `SET SESSION AUTHORIZATION` impersonation are
rejected before schema or role mutation, and the suspect physical connection
is destroyed instead of returning to a pool.

### 7.3 Database transaction

Bootstrap holds the destination-database transaction advisory lock
`pg_catalog.pg_advisory_xact_lock(1885827184, 1935763045)`—the fixed,
contract-version-independent `pgpp`/`safe` namespace—and one finite database
transaction. Every current or future bootstrap contract MUST take this same
key before inspecting or mutating safety objects, so versions cannot evade one
another by choosing different keys. It:

1. proves the target and roles;
2. creates `pgpipe_safety_owner`, or validates an exact existing fixed owner
   and performs only the locked password-null normalization;
3. creates both schemas directly under their final owners;
4. creates all seven protected tables in their final schema/shape;
5. grants the exact parameter privilege only to an owner created by this
   transaction (an accepted reused owner already has it), creates the exact
   activator, and marks the protected RI triggers `ENABLE ALWAYS`;
6. applies exact comments, row-type revocations, object ACLs, and the
   profile-specific runtime activator ACL;
7. seeds the destination identity and immutable replication profile;
8. removes every role membership created by bootstrap;
9. performs full catalog validation under the still-privileged administrator;
10. commits with `synchronous_commit = on`;
11. reconnects as the runtime role and proves every allowed operation and
    every denied owner-level capability required by the validation plan in
    savepoint-isolated, rollback-only probes that leave no safety evidence.

The advisory lock serializes one destination database; PostgreSQL roles are
cluster-wide. If independent first-runs in two databases concurrently attempt
to create `pgpipe_safety_owner`, the catalog uniqueness conflict aborts and
rolls back one bounded transaction. That attempt returns retryable
`repair_bootstrap_owner_race`; a fresh submission then validates and reuses the
winner under the exact shared-owner rules, including the stronger existing-role
authority requirement. pgpipe never catches the duplicate
inside an aborted transaction, never changes a nonconforming winner, and never
uses a database-local lock as a false claim of cluster-wide serialization.

Repair remains disabled throughout.

The attempt uses server-owned, non-overridable bounds:

| Bound | Locked v1 value |
| --- | ---: |
| Administrator connection acquisition | 10 seconds |
| Runtime connection acquisition | 10 seconds |
| Individual statement | 30 seconds |
| Database/advisory lock wait | 5 seconds |
| Whole database bootstrap after credential submission | 2 minutes |
| Local publish/reconciliation step | 10 seconds |

Human time entering/retrying a credential is outside the database-bootstrap
deadline and holds no database transaction, advisory lock, or owner membership.

### 7.4 Cross-resource publication

PostgreSQL DDL and a filesystem configuration cannot share one atomic
transaction. `pgpipe init` therefore uses an owner-only `0600` pending
configuration and initialization journal. It publishes the canonical
configuration atomically only after the database commit and runtime-role
verification succeed.

The canonical configuration directory must already be owned by the service
account and must not be group/other-writable (the package uses `/etc/pgpipe` at
`0750`). Init creates and validates a private `.pgpipe-init` child at `0700` on
that same local filesystem. It opens files relative to a pinned directory
descriptor with no-follow/exclusive-create semantics, accepts only
regular files with link count one and the expected owner, writes and `fsync`s
the file, atomically renames without crossing filesystems, and `fsync`s the
directory. It refuses symlinks, hard links, wrong owners/modes, replacement
races, and a world/group-writable parent. The journal contains identifiers,
fingerprints, phase, and sanitized result only—never either database password
or a DSN containing one.

If the database commit succeeds but publication or the browser response fails,
the same initialization attempt resumes by comparing its journal with the
exact current contract, destination identity, owner, shape, and ACL. It never
blindly repeats DDL and never treats a different pre-existing installation as
its own. Failure publishes no start instruction and never enables repair.

### 7.5 Existing objects

An exact pre-existing fixed owner role may be reused across databases as
defined above. A protected schema, old shared `pgpipe` safety object, partial
schema, wrong owner, or stale repair state is accepted only when the same
pending initialization journal proves it belongs to this exact attempt and
full validation succeeds. A nonconforming owner role returns
`repair_bootstrap_owner_conflict`; other old/partial state returns
`repair_safety_reset_required`.

There is no automatic drop, move, alter-owner, or data migration path. A future
on-disk or database upgrade requires a new reviewed contract; Phase 0 does not
speculate about it.

## 8. Browser credential boundary

The destination DBA credential may be collected only by the short-lived
`pgpipe init` wizard, never by the normal dashboard.

Required controls:

- before generating the random wizard origin/setup token or accepting any
  configuration or credential, the dedicated `pgpipe init` process disables OS
  core dumps and process crash-memory collection for its whole lifetime. On
  Linux this includes both a zero core-size limit and a non-dumpable process;
  other supported platforms require their equivalent. The process neither
  spawns a credential-bearing child nor re-enables dumpability. A failed or
  unverifiable protection closes the privileged listener and returns
  `repair_bootstrap_crash_protection_unavailable`; there is no override. Unit
  tests cover the platform adapter, and an integration test attests the live
  process protection before a canary credential is accepted;
- the entire one-shot `pgpipe init` wizard—not only its DBA card—uses
  `pgpipe-<26 lowercase unpadded-base32 characters>.localhost` from 128 random
  bits on loopback-only listeners. Hostname entropy creates a fresh browser
  origin even when a configured/fixed port is reused. Before printing or
  enabling the URL, pgpipe binds the configured port without address/port reuse
  on both `127.0.0.1` and `[::1]`, with an IPv6-only IPv6 socket, and serves the
  same setup session on both. Both binds MUST succeed; contract v1 fails closed
  rather than allowing the browser's IPv4/IPv6 selection to reach an unowned
  listener. It never binds a wildcard or non-loopback interface. A remote
  operator's SSH workflow must likewise reserve both local endpoints for the
  same forwarded port before showing the URL; partial single-family forwarding
  is unsupported. Dual-stack port-squatting and browser Happy-Eyeballs tests
  prove that a foreign listener on either family prevents wizard availability;
- the normal dashboard origin, direct remote exposure, and a reverse proxy
  cannot serve this wizard or credential endpoint;
- `pgpipe init` generates a separate 32-random-byte unpadded-base64url
  setup-session token, retains only its SHA-256 digest in process memory, and
  prints the exact random wizard URL and token once to the trusted terminal.
  The token is pasted at the wizard's access gate; it is never placed in the
  URL. Every protected `/api/setup/*` request sends it in `X-Setup-Token`;
  pgpipe compares fixed-size digests in constant time. The browser retains the
  token only in live JavaScript memory, so reload, navigation, or tab loss
  requires the operator to paste the terminal token again;
- the exact random `Host` and setup-session token are bound to the live
  `pgpipe init` process. This fresh origin is
  the primary defense against a service worker registered by an earlier local
  application. Before rendering the DBA password field, the page also requires no
  active `navigator.serviceWorker.controller` and no registration visible for
  its origin; a controller/registration closes the privileged listener and
  requires a new origin;
- contract v1 does not trust proxy forwarding for this credential endpoint;
  `Forwarded`, `X-Forwarded-*`, and similar headers never establish client
  address or scheme and are rejected on the privileged route. The setup HTTP
  server disables request-body/access logging for this route;
- a separate cryptographically random, expiring, single-use finalization token is
  bound to the authenticated setup session and exact pending-config
  fingerprint;
- exact `Host` and same-origin `Origin` checks, no CORS, bounded JSON/body size,
  content-type enforcement, request-rate limits, and double-submit
  idempotency are mandatory;
- responses use `Cache-Control: no-store`, `Pragma: no-cache`,
  `Referrer-Policy: no-referrer`, frame denial, and a setup-specific CSP with
  the exact directives `default-src 'none'; script-src 'self'; style-src
  'self'; connect-src 'self'; form-action 'none'; base-uri 'none'; object-src
  'none'; frame-src 'none'; frame-ancestors 'none'; worker-src 'none';
  manifest-src 'none'`. Script and style are packaged immutable same-origin
  assets; there are no third-party resources, inline script/style, eval,
  service-worker registration, HTML form submission, or base override;
- every dynamic reviewed configuration value, database/role name, status, and
  sanitized error is rendered only through text/value DOM APIs. Dynamic code
  MUST NOT use `innerHTML`, `outerHTML`, `insertAdjacentHTML`, `document.write`,
  template-to-HTML parsing, dynamic CSS, or untrusted selector/attribute names.
  Any unavoidable URL value is parsed and allowlisted to the exact setup origin
  before assignment. Meta refresh, `window.open`, and scripted or form
  navigation to a non-self origin are forbidden. Browser tests inject
  adversarial HTML, attribute, CSS, URL, Unicode, and control-character values
  into every reflected field and prove that no markup, navigation, request, or
  credential exfiltration occurs;
- the setup-session token, finalization token, and idempotency key exist only in
  the setup page's live JavaScript memory, never cookies, local/session storage,
  IndexedDB, cache, or form persistence. The password and finalization token are
  cleared best-effort immediately after submission/consumption. The
  idempotency key remains only until a saved result or journal reconciliation
  resolves the submission, then clears on that terminal result, rotation,
  close, or unload;
- raw setup-session tokens, finalization tokens, and idempotency keys are redacted
  before any middleware, telemetry, or error handling and MUST NOT enter
  request/access/error logs, traces, metrics, audit events, diagnostics,
  configuration, state, journals, or application crash-report context. The only
  plaintext emissions are the intentional one-time setup-session token print to
  the trusted terminal and the one-time finalization-token/idempotency-key response
  to its authenticated same-origin caller. Automated sink-scanning tests inject
  unique canary secrets through success, validation failure, database failure,
  timeout, replay, rate-limit, and panic-recovery paths and require all forbidden
  sinks to remain clean;
- pgpipe-controlled fields use password-appropriate autocomplete suppression,
  clear on submit and `pagehide`, and force a fresh privileged origin after a
  back/forward-cache restore. These are best-effort controls over pgpipe code;
  the product does not claim control over browser internals, extensions, a
  user-installed password manager, browser/OS crash images, swap, hibernation,
  terminal scrollback, or an operator's screen capture. Those external
  forensic surfaces are outside pgpipe's no-persistence claim; the setup UI
  warns the operator to use a trusted browser/host and relies on the secrets'
  bounded lifetime;
- the password is masked and is never put in a URL, command argument,
  environment variable, config, state, journal, browser storage, log, trace,
  metric, audit event, diagnostic, error, or response;
- the credential exists briefly in browser/request/process memory. pgpipe
  promises **not to persist it**, not impossible forensic zeroization;
- mutable buffers and fields are cleared best-effort, privileged connections
  close promptly, and the one-shot listener/token terminate after completion
  or timeout;
- a sanitized authentication/authorization failure keeps the wizard on the
  destination step, clears and focuses the password, and permits retry without
  repeating source/destination configuration by issuing a fresh token;
- the runtime dashboard exposes no DBA credential endpoint or form.

A new privileged-bootstrap submission is JSON no larger than 8 KiB. The DBA
username is 1-128 UTF-8 bytes after trimming; the DBA password is 1-2048 bytes
and is never normalized; and the finalization token is the canonical 43-character
unpadded-base64url encoding of 32 random bytes. A saved-result replay is a
different bounded envelope: it carries the idempotency key and the exact
non-secret attempt/revision/config/username bindings, and MUST omit both the DBA
password and consumed finalization token.

The privileged form and both finalization endpoints have a server-owned,
non-overridable 30-minute absolute lifetime beginning when the privileged card
is first enabled. A general `pgpipe init --timeout 0` does not extend it. At
expiry the server invalidates the setup privilege, finalization tokens,
idempotency keys, and idle privileged connections; it cancels and rolls back a
known-active bootstrap under the locked database deadlines. An uncertain
commit is journal-reconciled. No new privileged request is accepted until the
operator restarts explicit `pgpipe init`.

There is no ordinary-origin to privileged-origin handoff. The entire wizard,
including source and destination review, DBA credential entry, bootstrap, and
completion, remains on the exact fresh origin printed by `pgpipe init`. The
normal dashboard's login/session credential is never accepted by this origin,
and the setup-session token is never accepted by the normal dashboard. The
page has no opener, accepts no `postMessage`, and trusts no target-origin value
supplied by browser script.

`POST /api/setup/finalization-token` is a token-issuing mutation. It
requires the same-origin `X-Setup-Token` setup-session credential and all
transport/Host/Origin checks,
contains no DBA credential, and returns once both a 32-random-byte finalization
token and a separate server-generated 32-random-byte base64url idempotency key.
The server retains SHA-256 digests of the independently generated 256-bit
random values, not plaintext. This is safe because the input is full-strength
random capability material rather than a human password. Both values are bound
to the current in-memory candidate ID/revision/fingerprint, issuing setup
session, issue time, and 10-minute expiry. The stable durable bootstrap attempt
ID is allocated during the atomic pre-`202` staging boundary and must match the
admitted operation and recovery journal. A token retry rotates both capabilities
and invalidates the prior pair; a separate public submission counter provides
no additional authority. Collision scope is the whole live setup process and
generation retries on the practically impossible existing-digest collision.

`POST /api/setup/finalize` consumes that exact revision before opening the DBA
connection, returns `202 Accepted`, and supplies a setup-token-gated,
DBA-credential-free `GET /api/setup/finalize/{id}` polling path. The server
saves the bounded result under the submission idempotency key atomically with
consumption. An exact replay returns the saved result without DBA password or
finalization token. Request processing
first enforces the same-origin `X-Setup-Token` setup-session authentication,
transport, Host, Origin, content/body bounds, generic request-rate limit, and
then performs the bound idempotency-key lookup. A live exact saved record is
returned at that point after attempt, revision, pending-config fingerprint,
expiry, and normalized DBA username match. Only when there is no saved record
does the server require and validate the new-submission DBA password and
finalization token before atomically consuming that token/revision. Replay
equality never saves or hashes the DBA password. Reuse with different
non-secret bindings, or supplying password/token fields in a replay envelope,
fails uniformly. Any different use of a consumed, rotated, expired, or wrong
token fails uniformly. An exact saved replay does not consume the five-attempt
DBA-authentication budget, although the generic request-rate limit still
applies. A lost/ambiguous response is reconciled through the stable bootstrap
attempt and journal, never by rerunning DDL.

After a credential/authority failure, or another retryable failure known to
have rolled back before commit, the wizard clears the password and explicitly
issues a new token and a new submission idempotency key while retaining the
same candidate revision, stable bootstrap attempt, and reviewed ordinary
configuration. An
ambiguous commit does not enter this path; it reconciles the journal. No GET
returns or recovers token material. At most five failed DBA authentication
attempts are accepted per setup session in 15 minutes; later attempts return
`429 repair_bootstrap_rate_limited` with an integer `Retry-After`.

The non-browser automation path uses a protected inherited file descriptor or
service-manager credential, never argv or an environment variable.

## 9. Validation placement and performance contract

Validation is catalog work, not replication work.

- Full validation runs during initialization and the explicit diagnostic
  command. Password-null verification is privileged as described in the role
  contract; an unprivileged diagnostic reports it as bootstrap-attested rather
  than freshly observed.
- Startup performs the same complete manifest validation described below,
  within its larger bounded read-only deadlines, before it reports readiness.
- Enabling repair and claiming a repair require a current successful complete
  manifest proof; each operation performs a fresh validation rather than
  trusting a stale watcher result.
- A lightweight watcher revalidates the complete safety manifest on a fixed
  delay: the first pass starts 60 seconds after successful startup validation,
  and each later pass starts 60 seconds after the previous pass completes.
  The watcher starts only after destination identity and the active ownership
  fence are bound, before snapshot/backfill begins, and validates the manifest
  against that current destination identity/ownership token. Each complete pass
  is bounded to 30 seconds and passes never overlap. The watcher checks observable fixed-owner attributes/
  comment/settings plus password-null bootstrap attestation,
  runtime-role attributes and actual maintenance-session identity, membership
  and ADMIN/SET/predefined-role reachability, owner closure/dependency chains,
  protected/runtime schema ownership/ACL/comments, all seven table definitions/
  comments/indexes/constraints/identity row, exact protected ACLs, runtime
  database and parameter ACL/default/effective value, and routine-execution
  baseline. It runs one complete read-only validation pass on one maintenance
  connection; it does not open one connection or schedule one query per table.
  Connection acquisition and every validation statement are bounded to five
  seconds. It does not block the replication reader and writes no durable state
  when the result is unchanged. A permanent repair-only mismatch immediately
  blocks new tokens, admissions, and claims and stops an active repair before
  its next fenced chunk; ordinary streaming may continue only after a fresh
  core-manifest validation succeeds. A permanent core-safety mismatch cancels
  the pipeline immediately. The first transient failure sets repair safety to
  `unavailable`, prohibits new repair control operations, and stops an active
  repair before its next fenced chunk; a repair transaction already open may
  finish under its existing deadline. Ordinary streaming may continue through
  one or two consecutive transient failures. Three consecutive failures cancel
  the pipeline before more destination work is scheduled. A transaction already
  open when cancellation is signaled remains bounded and may commit; the design
  does not claim retroactive prevention. A successful check resets the counter
  and restores repair readiness only when every other live admission condition
  is also healthy. Explicit enablement, token, admission, and claim checks
  publish current safety truth but do not advance the scheduled watcher's
  consecutive-transient strike counter.
- Repair chunk transactions retain their existing fence, generation, proof,
  and physical-relation checks because those checks protect a repair commit.

**No per-event ownership checks are allowed.** In particular, the design MUST
NOT add a catalog query, role lookup, ACL lookup, ownership hash, state read or
write, allocation, or new ownership-validation branch for each decoded row,
event, source transaction, ordinary destination transaction, or grouped
streaming commit. A watcher signals the existing pipeline cancellation path;
it does not insert checks into the hot apply loop.

Performance certification MUST show no material steady-state replication
regression with repair disabled or enabled-but-idle.

Startup safety validation has a 10-second connection-acquisition limit, a
15-second statement limit, and a 30-second whole-validation limit. A permanent
preparation/reset/owner/shape/ACL/dependency failure exits with status 78. A
transient connection, catalog, or deadline failure exits with status 75. When
repair is disabled and only the two repair-specific relations are invalid, the
process may start normal replication after all core checks pass, but exposes
repair as `invalid`; every other permanent core failure blocks startup.

## 10. Readiness and startup failure behavior

The public result keeps four kinds of truth separate:

1. `safety_status` is the latest complete manifest result. It says whether the
   protected destination boundary is prepared; it is not the saved switch.
2. `execution_enabled` is the live value held by the running process.
3. `saved_execution_enabled` is the desired value durably saved in the
   configuration file. `restart_required` means saved and live lifecycle truth
   do not yet agree, including the one-way disable latch.
4. `approval_available` is admission authority. It is true only when safety,
   authentication/CSRF, ownership, streaming, reconciliation, worker, saved
   configuration, and live configuration are all ready.

Changing one field never rewrites the meaning of another. For example, a valid
manifest remains `prepared` during a source outage, while
`approval_available=false` and runtime `readiness=not_streaming` explain why no
new repair may be admitted.

Closed `safety_status` values:

| Value | Meaning |
| --- | --- |
| `required` | The greenfield manifest is wholly absent or has never been prepared |
| `prepared` | Owner, schema, objects, ACL, identity, and runtime access passed |
| `invalid` | Current-contract ownership, shape, ACL, identity, or dependency is unsafe |
| `unavailable` | Validation could not complete because the destination/catalog was unavailable |

Client-only **Checking readiness** and **Readiness unavailable** placeholders
are loading/error views, not additional server lifecycle states. Unsupported
pre-contract or partial state is reported through `safety_status=invalid` plus
its stable reset/definition reason code. Once this process has proved the
manifest prepared, a later missing owner, schema, or protected object is damage
and remains `invalid`; it never returns to the first-installation `required`
state.

The API exposes `safety_status` separately from execution `readiness`. Repair
approval is available only when both are `prepared`/`ready` and saved/live
enablement agree.
The greenfield API remains `/api/v2/repair`, reports
`contract_version: "repair-approval-v2"` and `contract_revision: 3`, and
requires the new fields. Revision-1/revision-2 compatibility projections and
the unversioned repair route family is unsupported and removed rather than
retained as migration stubs.

Execution `readiness` is one of `disabled`, `starting`, `setup_required`,
`not_streaming`, `reconciling`, `recovery_required`, or `ready`. Restart is a
separate configuration/lifecycle fact, never a `readiness` value.

The seven and only seven server lifecycle states are:

| `lifecycle_state` | Exact dashboard label | Meaning |
| --- | --- | --- |
| `safety_setup_required` | **Safety setup required** | The greenfield manifest is wholly absent or has never been successfully prepared; repair cannot be enabled. Missing or damaged objects after preparation are invalid instead. |
| `safety_setup_invalid` | **Safety setup invalid** | Storage exists but its contract, ownership, privileges, identity, or dependency closure is unsafe. |
| `prepared_repair_off` | **Prepared — repair off** | Safety is valid and both saved and live repair execution are off. |
| `enablement_saved_restart_required` | **Enablement saved — restart required** | The enabled setting is durable, but the running process remains off and cannot admit or claim work. |
| `repair_enabled_ready` | **Repair enabled — ready** | Saved/live enablement, safety, security, ownership, streaming, reconciliation, and worker readiness all pass. |
| `disablement_saved_restart_required` | **Disablement saved — restart required** | The disabled setting is durable; new admissions and claims are already blocked, and restart completes the live transition. |
| `repair_unavailable` | **Repair unavailable** | Configuration, live runtime, safety validation, recovery, or security truth cannot currently authorize repair; use `reason_code` for bounded remediation. |

The bounded capability projection is exactly:

```json
{
  "api_version": "v2",
  "contract_version": "repair-approval-v2",
  "contract_revision": 3,
  "execution_enabled": false,
  "saved_execution_enabled": false,
  "saved_configuration_available": true,
  "approval_available": false,
  "restart_required": false,
  "readiness": "disabled",
  "safety_status": "prepared",
  "lifecycle_state": "prepared_repair_off",
  "reason_code": "repair_execution_disabled",
  "cancellation_mode": "pre_claim_only"
}
```

`restart_guidance` is omitted when `restart_required=false`. When restart is
required, it contains exactly:

```json
{
  "systemd": "sudo systemctl restart pgpipe",
  "docker": "docker compose restart pgpipe"
}
```

There are no database-administrator fields, administrator credential endpoint,
or runtime DDL action on the normal dashboard. It changes only the saved
`repair.execution_enabled` value after authenticated validation. Operators
complete either transition with the command appropriate to their deployment:

```bash
sudo systemctl restart pgpipe
```

```bash
docker compose restart pgpipe
```

Enabling always performs a fresh bounded live safety and runtime check before
the setting is saved. A successful save does not activate repair in the
current process; approval and claims stay blocked until restart validates the
manifest again and reaches `repair_enabled_ready`.

Disabling takes effect in two stages. Once the disabled setting commits, a
one-way in-process latch immediately blocks every new token, admission, and
claim; saving `true` again cannot re-arm that process. Restart is still required
to load the disabled runtime configuration. An already claimed repair is not
canceled by the save: it may finish under its existing durable limits. If the
operator restarts before it finishes, shutdown/restart uses the existing
destination fence, chunk ledger, state, and journal to classify it as completed
or one of the conservative interrupted outcomes. It is never silently resumed
or reported as canceled; partial, ambiguous, or post-write evidence remains
recovery-required.

| Condition | Normal replication | Repair behavior |
| --- | --- | --- |
| All seven valid; saved and live flag off | Starts | `prepared_repair_off`; repair off |
| Enablement saved while live flag is off | Continues | `enablement_saved_restart_required`; no token, admission, claim, or premature activation before restart |
| Disablement saved while live flag is on | Continues | `disablement_saved_restart_required`; new token/admission/claim authority is blocked immediately and remains latched until restart |
| All seven valid; flag on; worker/pipeline ready | Starts | Repair enabled and ready |
| Repair-only fence/ledger missing or unsafe; flag off | May start only after all core checks pass | Repair invalid and cannot be enabled |
| Repair-only fence/ledger missing or unsafe; flag on | Startup blocked | No API admission or worker |
| Identity, pipeline fence/audit, apply ledger, rebuild ledger, owner boundary, or protected schema unsafe | Startup blocked regardless of repair flag | Fail closed |
| Unsupported old/partial development objects | Startup blocked | `safety_setup_invalid` with the stable reset reason; nothing is changed automatically |
| Durable current-version interrupted/recovery evidence | Existing recovery policy blocks affected startup/work | Recovery required; evidence is preserved |
| Transient validation failure | Startup blocks because safety cannot be proven | `unavailable`, retryable |
| Post-start repair-only tampering | Ordinary streaming may continue only if core safety remains proven | Stop new tokens/admissions/claims; active repair stops at the next fenced boundary |
| Post-start core-safety tampering | On detection, cancel the pipeline and schedule no new destination work; an already-open bounded transaction may commit | Repair invalid |

Permanent startup failures exit nonzero with the stable code, sanitized object,
expected condition, and remediation in the journal. Startup never fixes ACLs,
transfers ownership, creates safety objects, falls back to an owner, or silently
disables an explicitly enabled feature.

## 11. Stable validation and API errors

Every error response uses the existing bounded JSON envelope. It contains one
stable `code`, a sanitized human message/remediation, and at most a sanitized
role/object identifier. It never contains SQL, a DSN, credential, raw database
error, row content, proof, token, or full job/configuration object.

While the setup session and idempotency binding remain live, an exact replay
returns its saved response. After expiry it fails uniformly and the stable
attempt is reconciled through the journal rather than extending token life.
`Retryable=Yes` means the workflow may retain the reviewed candidate revision
while creating a fresh token and idempotency key after the stated condition or
`Retry-After`, provided the previous attempt is proven pre-commit/rolled back.
`Retryable=No` may still
permit a human to correct input and create a new submission where the specific
error says so; it forbids automatic replay of the failed request. Ambiguous
post-commit evidence is reconciled, never retried as new work.

### 11.1 Safety validation codes

| HTTP | Stable code | Retryable | Meaning |
| ---: | --- | :---: | --- |
| 503 | `repair_safety_preparation_required` | No | Fresh safety initialization has not completed |
| 503 | `repair_safety_postgres_version_unsupported` | No | The PostgreSQL major is outside contract v1's reviewed 15-18 range |
| 409 | `repair_safety_reset_required` | No | Unsupported old, partial, or foreign safety state exists |
| 503 | `repair_safety_owner_missing` | No | The fixed protected owner is absent |
| 503 | `repair_safety_owner_attributes_invalid` | No | Owner attributes do not match the contract |
| 503 | `repair_safety_owner_membership_unsafe` | No | Runtime can inherit, assume, or administer protected authority |
| 503 | `repair_safety_runtime_role_invalid` | No | Runtime role attributes/session identity are unsafe |
| 503 | `repair_safety_database_acl_invalid` | No | Runtime database ownership or effective database privileges violate the contract |
| 503 | `repair_safety_replication_profile_mismatch` | No | The configured canonical profile differs from the immutable destination identity |
| 503 | `repair_safety_runtime_parameter_acl_invalid` | No | Parameter ACLs/defaults, the protected activator, or the live session mode violate the selected profile |
| 503 | `repair_safety_runtime_routine_acl_invalid` | No | Runtime can execute a routine outside the locked PostgreSQL baseline or safe application boundary |
| 503 | `repair_safety_schema_missing` | No | A required schema is absent |
| 503 | `repair_safety_schema_owner_mismatch` | No | A schema has the wrong owner |
| 503 | `repair_safety_object_missing` | No | A required protected object is absent |
| 503 | `repair_safety_object_owner_mismatch` | No | A protected object has the wrong owner |
| 503 | `repair_safety_acl_missing` | No | A required runtime grant is absent |
| 503 | `repair_safety_acl_excessive` | No | Runtime/PUBLIC/another role has forbidden authority |
| 503 | `repair_safety_definition_mismatch` | No | Columns, defaults, constraints, indexes, or comments differ |
| 503 | `repair_safety_dependency_invalid` | No | A forbidden dependency or executable behavior exists |
| 503 | `repair_safety_destination_identity_invalid` | No | Destination singleton identity is absent, malformed, or mismatched |
| 503 | `repair_safety_ownership_fence_invalid` | No | The active pipeline ownership row is absent or no longer matches the current owner generation |
| 503 | `repair_safety_validation_unavailable` | Yes | A bounded validation could not complete |
| 503 | `repair_safety_readiness_interrupted` | No | A scheduled or fresh safety observation invalidated an active repair before its next fenced chunk; its durable terminal evidence requires operator review |
| 409 | `repair_restart_required` | No | Saved enablement differs from the running process |

### 11.2 Initialization endpoint codes

| HTTP | Stable code | Retryable | Meaning |
| ---: | --- | :---: | --- |
| 400 | `repair_bootstrap_request_invalid` | No | Body, content type, bounds, or fields are invalid |
| 401 | `repair_bootstrap_session_invalid` | No | Setup-session token is missing, expired, or bound to another live init process/origin |
| 409 | `repair_bootstrap_token_invalid` | Yes | Finalization token is missing, expired, consumed, or wrong; request a fresh token in the same live setup session |
| 503 | `repair_bootstrap_crash_protection_unavailable` | No | The init process could not prove that OS core/crash-memory collection is disabled |
| 403 | `repair_bootstrap_transport_forbidden` | No | Privileged collection is exposed over a forbidden transport |
| 403 | `repair_bootstrap_origin_forbidden` | No | Host/origin does not match the one-shot setup session |
| 403 | `repair_bootstrap_database_transport_forbidden` | No | The DBA connection is not TCP with verify-full TLS to the reviewed destination |
| 422 | `repair_bootstrap_postgres_version_unsupported` | No | The destination PostgreSQL major is outside contract v1's reviewed 15-18 range |
| 422 | `repair_bootstrap_admin_authentication_failed` | Yes | Destination DBA credentials were rejected; credential may be retried |
| 422 | `repair_bootstrap_authority_insufficient` | Yes | DBA authenticated but lacks required bounded authority; a different credential may be retried |
| 409 | `repair_bootstrap_target_mismatch` | No | Runtime and DBA sessions are not the same live database |
| 409 | `repair_bootstrap_owner_conflict` | No | A pre-existing owner role violates the fixed contract |
| 409 | `repair_bootstrap_owner_race` | Yes | Another database concurrently created the shared fixed owner; retry validation after rollback |
| 409 | `repair_bootstrap_config_changed` | No | Reviewed pending destination configuration changed |
| 409 | `repair_bootstrap_in_progress` | Yes | Another exact initialization attempt holds the global lock |
| 429 | `repair_bootstrap_rate_limited` | Yes | The bounded credential-attempt budget is exhausted |
| 504 | `repair_bootstrap_timed_out` | Yes | The bounded attempt expired; reconcile before retry |
| 500 | `repair_bootstrap_result_ambiguous` | No | Commit/publication outcome is uncertain; resume the same attempt |
| 503 | `repair_bootstrap_unavailable` | Yes | Destination connection or catalog is temporarily unavailable |

API handlers MUST NOT collapse permanent ownership/shape/ACL failures into a
generic destination-unavailable code. Concurrent or repeated requests return
the same deterministic result for one bootstrap attempt; they do not create
parallel DDL attempts.

At process startup, every permanent `repair_safety_*` failure above exits with
status 78, except `repair_safety_validation_unavailable`, which exits with
status 75. `repair_restart_required` is an HTTP/runtime-capability response for
a saved/live configuration difference and is not emitted as a startup exit:
after restart, the new saved configuration either validates and becomes live or
startup returns its underlying stable safety code.

## 12. Threat model and required response

| Threat/failure | Required control/result |
| --- | --- |
| Spoofed/stale setup page, alternate-family port squatting, or reflected-field injection | Fresh random whole-wizard origin owned on both loopback families, terminal-issued setup-session token, config fingerprint, Host/Origin and service-worker checks, locked CSP/navigation, text-only dynamic rendering, and adversarial browser tests; reject before credential use |
| Browser double submission or disconnect | Idempotent attempt; one DB transaction; resume from journal |
| DBA and runtime target different databases | Live same-destination proof before DDL |
| DBA credential or setup/bootstrap/idempotency secret disclosure | One-shot protected transport; init-process core/crash-memory collection disabled before secrets; pre-middleware redaction; no forbidden application persistence/logging/telemetry/crash context; sink-scanning tests; bounded lifetime and explicit external browser/OS forensic boundary |
| Runtime tries protected-object `DROP`, `ALTER`, `TRUNCATE`, a direct protected ACL grant, or `SET ROLE` to the owner | PostgreSQL denies the owner/DDL operation; a notice-only `GRANT` must make no ACL change; automated negative tests prove the boundary |
| Runtime receives excess privilege indirectly | Role graph and ACL validation fail closed |
| Partial or old development schema | `repair_safety_reset_required`; no automatic mutation/deletion |
| Database commit succeeds but config publication fails | Same-attempt journal reconciliation; repair remains off |
| Protected object is tampered with after startup | Background validation blocks repair or cancels the pipeline according to the severity matrix |
| Superuser/DBA intentionally changes evidence | Outside the owner boundary; detected where possible, never claimed as prevented |
| Compromised runtime misuses or proxies allowed DML/activator authority | Outside instantaneous DDL ownership prevention; complete manifest validation detects forbidden proxy objects, while fencing, CAS, proofs, audit, cancellation, and recovery checks remain mandatory |

## 13. Phase 0 acceptance and sign-off

Phase 0 is complete only when all boxes are satisfied:

- [x] protected and runtime schema names are fixed;
- [x] protected owner name and attributes are fixed;
- [x] all seven tables, exact ACLs, comments, constraints, indexes, foreign
  keys, and allowed dependencies are defined;
- [x] readiness, startup, initialization, and post-start failure behavior are
  defined;
- [x] browser credential collection and retry are bounded and non-persistent;
- [x] no in-place migration or backward-compatibility work remains in this
  greenfield design;
- [x] old development objects are unsupported, non-adopted, and disposable;
- [x] stable validation and API codes are defined;
- [x] the contract explicitly forbids per-event ownership checks;
- [x] security reviewer approval recorded below;
- [x] correctness reviewer approval recorded below.

| Review | Status | Date | Notes |
| --- | --- | --- | --- |
| Security | Approved | 2026-09-01 | Role graph, exact ACL/dependency boundary, browser-secret/crash boundary, stable failures, and PostgreSQL 14 exclusion approved after independent review |
| Correctness | Approved | 2026-09-01 | PostgreSQL 15-18 manifest, RI triggers, session activator, bootstrap transaction, startup matrix, publication, and recovery boundaries approved after independent review |

Any later change to schemas, owner attributes, ACLs, object shape, readiness,
credential handling, greenfield scope, or hot-path placement reopens Phase 0
security and correctness review.

## 14. Phase 6 security, regression, and performance matrix

Phase 6 is an executable release gate, not a checklist accepted from code
inspection. The tested runtime session is the configured destination login
(`pgpipe_writer` in the shipped fixtures), connected directly rather than
through an administrator session using `SET ROLE`. Each owner/DDL operation
must produce a PostgreSQL privilege failure, and every test must prove that the
target object, ACL, role graph, and manifest remain unchanged afterward.
PostgreSQL may acknowledge a `GRANT` issued without grant option while warning
that no privileges were granted; that case passes only when the ACL is proven
byte-for-byte unchanged.

### 14.1 Protected-boundary SQL tests

The supported-PostgreSQL matrix runs the following negative operations as the
runtime login and requires every owner, DDL, and role operation to fail. A
`GRANT` form that PostgreSQL accepts with a “no privileges were granted” notice
must instead be proven completely ineffective by an unchanged byte-for-byte
ACL:

- creating a table in `pgpipe_safety`;
- truncating or dropping a protected table;
- dropping a protected index or the protected schema;
- dropping a protected constraint;
- changing protected-object ownership;
- adding a trigger or rule to a protected table;
- granting protected-table privileges to itself or another role;
- assuming `pgpipe_safety_owner` with `SET ROLE`;
- creating either a direct or indirect membership path to the protected
  owner.

The same live test derives its positive DML matrix from the versioned manifest.
Every listed table privilege must succeed in a rolled-back transaction, and
every table privilege omitted by the manifest must fail. This prevents the
test from silently diverging when a future contract revision intentionally
changes an allowlist. Successful DML is not proof that arbitrary row values
are valid: table constraints, fencing, proofs, and compare-and-set conditions
remain authoritative.

PostgreSQL 17 and 18 add the table-level `MAINTAIN` privilege. Their matrix
requires `has_table_privilege` to report it absent for every protected table
and executes `REINDEX TABLE` as the runtime login to prove that a real
maintenance operation is denied. PostgreSQL 15 and 16 run the remaining exact
matrix without asking their catalogs about the not-yet-defined privilege.

PostgreSQL 15 exercises the legacy membership semantics used by the contract.
PostgreSQL 16, 17, and 18 exercise the non-inheritable, non-administrable grant
form. PostgreSQL 14 is destination-unsupported and must be rejected before any
role or schema mutation. A dedicated PostgreSQL 14 integration test invokes the
complete bootstrap twice and proves the stable unsupported-version result leaves
the role, membership, schema, relation, routine, default-ACL, and database-ACL
snapshot unchanged. Its source-only support remains independent. A Phase 6
result must never present PostgreSQL 14 as a supported protected destination.

### 14.2 Workflow and secret tests

The Phase 6 aggregate binds the existing streaming, grouped-write, ownership
fence, restart recovery, table-rebuild, repair, verification, and setup
integration suites to the new boundary tests. It additionally requires direct
coverage for:

- idempotent fresh bootstrap and retry after rejected DBA authentication;
- wrong-destination affinity rejection before DDL or configuration
  publication;
- role-creation denial without an owner fallback;
- disconnected and duplicate browser submissions producing one bootstrap;
- one-time token consumption and replay rejection;
- post-install tampering rejected by startup and pre-repair admission; and
- unique temporary DBA-credential sentinels absent from configuration, files,
  process environment, durable state, logs, diagnostics, API/status bodies,
  and browser storage. A setup/finalization token or idempotency value may
  cross only its deliberate one-time issuance/submission boundary; after that
  boundary, its plaintext must be absent from status/detail responses, logs,
  metrics, durable state, diagnostics, environment, and browser storage.

Secret tests prove pgpipe-owned sinks only. They do not claim that an external
browser, operating system, database server, reverse proxy, or administrator
can never retain forensic evidence; those external boundaries remain governed
by the deployment policy in Sections 7 and 12.

### 14.3 Performance gate

The performance gate uses an immutable pgpipe image, bootstrap image, source
tree fingerprint, PostgreSQL image, configuration apart from the tested repair
switch, seed, workload, and host. It runs an even count of at least four exactly
alternating pairs (odd pair IDs disabled-first, even pair IDs enabled-first)
of normal replication with repair disabled and repair enabled but idle. Every
lane must pass the normal logical, raw-storage, schema, LSN, accounting,
grouping, routing, drain, resource, and final-status gates before its throughput
is eligible for comparison.

The harness computes each pair's enabled-idle throughput delta from its matched
disabled observation. The median of those within-pair deltas may be no lower
than -2%; the separate lane medians are descriptive only. Both lanes must also
meet the locked variance bound, and source offered-load plus the native control
must remain within the per-pair drift/noise bounds; otherwise the result is
`INCONCLUSIVE_NOISY`, not a pass or regression. Active-repair throughput is a
separate capacity diagnostic because it deliberately competes for destination
resources and cannot certify steady-state overhead.

Evidence must prove:

- no full destination-safety manifest validation or repair-readiness catalog
  lookup was added per row, event, source transaction, destination transaction,
  or grouped commit;
- repair readiness work adds no source connection;
- safety validation remains cold-path work; and
- repair-active, job-total, and action-total evidence is absolute zero before
  and after every enabled-idle workload, not merely unchanged.

The connection proof is an exact-four assertion over five-second steady-state
samples and therefore detects sustained, not arbitrarily brief, extra sessions.
Reported source, pgpipe end-to-end, and native end-to-end rates are independently
recomputed from raw event counts and monotonic workload/completion signals;
completion totals must equal workload duration plus the slower convergence or
slot-ack signal.

For clarity, request-bound full validation occurs during initialization,
startup, explicit enablement, and repair admission. The fixed-delay,
destination-only tamper watcher required by Section 9 also remains in force.
Neither mechanism may run from the replication event loop. Removing that
watcher would weaken the Phase 4 security contract and therefore is not an
acceptable interpretation of the Phase 6 performance requirement.

The executable static proof has a deliberately reviewable boundary. It locks
named safety validators and repair-readiness entry points to exact callers
across `internal/`, then recursively follows local calls from every normal
event/group writer and rejects newly reachable catalog-query literals and
constants. The two exact pre-existing exceptions are the locked hybrid target
capability proof and ownership-fence destination-identity failure diagnosis;
apply-ledger DML is explicitly required to remain reachable. This does not
claim that normal DML uses no `pg_catalog` types/functions, or that static AST
analysis can classify arbitrary SQL assembled dynamically or through
reflection.

The harness is destructive only after its exact acknowledgement. It runs from a
private clone of the accepted `HEAD`, uses a dedicated allowlisted two-database
Compose topology that rejects external storage/networking and service namespace
or privilege escape fields, labels every disposable resource with an unpredictable owner
nonce, routes benchmark database commands through revalidated immutable
container IDs, and removes only exact verified IDs/names carrying that nonce. Cleanup
failure invalidates success. It retains a sanitized, checksummed evidence
directory whose comparator and final run status must independently agree on
`PASS`. A code-complete Phase 6 is not a
release performance certification until this live repeated gate passes on the
unchanged release candidate and reviewers approve the resulting evidence.

## 15. Phase 7 documentation and release certification

Phase 7 makes the greenfield model understandable and binds publication to the
exact release candidate. Documentation or a successful development test is not
release certification. A gate may move from **Pending** only when its evidence
is bound to the unchanged candidate commit and reviewed; human approval gates
also require the reviewer and date. `INCONCLUSIVE`, skipped, stale, expiring, or
different-SHA evidence is not a pass.

### 15.1 Requirements and current-development register

This table is the canonical source-controlled requirements and
current-development register. It is intentionally **not** the mutable result
certificate for an already-created tag. A `Pending` row is truthful for the
current development tree and cannot be changed merely because a workflow
definition exists. Large or sensitive evidence may remain access-controlled,
but every accepted receipt must retain its checksums and exact candidate
identity.

| Gate | Current status | Required release evidence |
| --- | --- | --- |
| Documentation | Implemented in the development candidate; final review pending | Identity matrix, fresh Docker/Linux install, managed-service role preparation, exact ACLs, enable/restart lifecycle, repair/recovery, tamper errors, backup/PITR, owner rationale, application ownership profiles, future upgrade policy, and explicit reset/breaking guidance agree across shipped documents |
| Security review | **Pending final-candidate approval** | Named reviewer/date, no unresolved high-severity security finding, and exact-candidate security/supply-chain results |
| Correctness and recovery review | **Pending final-candidate approval** | Named reviewer/date, PostgreSQL/state/fencing/failure evidence, and a completed operator recovery drill |
| Browser UX and accessibility review | **Pending final-candidate approval** | Named reviewer/date plus the exact-candidate real-browser workflow and accessibility results |
| Go and race tests | **Pending exact-candidate run** | Full Go, race, static, and contract outputs bound to the candidate commit |
| PostgreSQL and integration tests | **Pending exact-candidate run** | Supported-version destination matrix, PostgreSQL 14 rejection/source coverage, state backends, streaming, rebuild, verification, repair, and failure-injection results |
| Package and raw-binary installation | **Pending clean-host evidence** | Fresh Ubuntu DEB, Fedora RPM, systemd, and raw-binary initialization results; package scripts prove that they never receive a DBA credential or mutate the database |
| Docker installation and operation | **Pending exact-candidate run** | Fresh automatic bootstrap, repair-off startup, explicit stale-volume refusal/reset, Docker integration, and complete browser repair workflow results |
| Soak and operational recovery | **Pending** | Same-candidate destructive soak, no duplicate execution/audit gap, and a reviewed partial/ambiguous recovery exercise |
| Performance | **Pending** | The unchanged-candidate paired gate reports `PASS`, satisfies the maximum 2% steady-state target, and retains checked evidence as specified in section 14.3 and `PERFORMANCE.md` |
| High-severity findings | **Pending final triage** | Open-finding register proves that no unresolved high-severity issue remains |
| Clean-breaking major release | **Pending tag and publication approval** | The tag is a new major version; release notes carry the breaking/reset warning; signed artifacts, packages, and image are bound to the certified commit |

### 15.2 Non-circular exact-SHA certificate lifecycle

The exact release-candidate tag, not a later documentation commit, identifies the
candidate being certified. The release proceeds in this order:

1. Finalize the code, requirements register, changelog, and breaking/reset
   notice, enable GitHub immutable releases, then create the candidate's
   major-version tag. The workflow repeatedly revalidates the tag before
   publication; GitHub locks it when the immutable release is published.
2. The tagged workflow runs every automated gate against that exact tag SHA.
   The aggregate automated-gates receipt records the repository, tag, commit
   SHA, run ID/attempt, and result of every required gate. Performance and soak
   retain their detailed checksummed evidence separately, and the release
   workflow independently binds the built binary and published image to the
   certified binary hash before packaging all evidence. A receipt or evidence
   artifact from another SHA/run/attempt, a skipped result, or an expired
   artifact without a retained verified copy is ineligible.
3. Five separate protected GitHub Environment deployments collect the security,
   correctness/recovery, browser UX/accessibility, operations, and performance
   decisions. The workflow validates the actual environment and deployment-tag
   policy before approval and again after all decisions. It accepts a decision
   only when GitHub's workflow-run approval-history API identifies exactly one
   separate `approved` event from a directly configured reviewer who did not
   initiate the workflow. The approval's stable numeric GitHub user ID and login
   must both match that configured direct reviewer. Repository, organization,
   and environment variables are never policy proof. Missing API authority or
   policy fails closed.
4. Only after all exact-SHA receipts and all five protected deployment approvals
   succeed may the workflow assemble, sign, provenance-attest, and publish the
   immutable release evidence/artifact bundle and GitHub Release. The signed
   bundle contains the API policy snapshots, approval-history responses,
   workflow receipts, detailed security evidence, and their checksums. The fixed
   release evidence filename is
   `pgpipe-<version>-release-gate-certification.tar.gz`; it contains the
   validated Phase 6 automated-gates receipt, `approvals/certificate.tsv`, all
   five exact-run approval receipts, and the retained policy/security evidence
   before checksum, signing, and attestation.
   Immediately before GitHub Release creation, the workflow rechecks the actual
   immutable-release repository setting; immediately afterward, it requires
   GitHub to report the published release as non-draft and immutable. The image
   job passes its verified, signed, and attested OCI index digest directly to
   this publication job, which records the exact
   `ghcr.io/<owner>/<repository>@sha256:<digest>` reference in the immutable
   GitHub Release body and reads it back before accepting publication.
5. After publication, a later commit on `main` may append a historical record
   linking the immutable tag, tag SHA, GitHub Release, retained evidence bundle,
   and deployment audit. That history update does not modify the tag contents,
   does not certify the later documentation commit, and does not require
   pretending the tag's source copy of this table was already `Passed`.

GHCR does not give this workflow an atomic create-if-absent or immutable-tag
operation. It therefore does not publish a semantic-version image tag. The
minor-version and `latest` tags are explicitly mutable rolling conveniences,
never a security or release-completion boundary. The signed and
provenance-attested OCI digest recorded in the immutable GitHub Release is the
version-specific, authoritative deployment reference. Operators must pin that
digest when authenticity or reproducibility matters.

The five protected review identities are fixed:

| Review domain | Protected GitHub Environment |
| --- | --- |
| Security | `production-security-review` |
| Correctness and recovery | `production-correctness-recovery-review` |
| Browser UX and accessibility | `production-browser-ux-accessibility-review` |
| Operations | `production-operations-review` |
| Performance | `production-performance-review` |

Each environment requires one to six directly named domain reviewers, prevents
self-review, and has exactly one custom tag policy, `v*.*.*`, with no branch
policy. Team-only reviewers are excluded because the read-only audit authority
cannot prove private team membership. Administrators must disable and manually
review the bypass setting because GitHub's documented environment response does
not expose it. The workflow compensates by requiring an API-recorded approval
from a configured reviewer; it also rejects workflow reruns because approval
history is not attributed to a run attempt. GitHub immutable releases must be
enabled, and the repository supplies a dedicated read-only policy-audit token as
documented in
[CI.md](CI.md#required-protected-github-environments).

This avoids a circular process in which recording approval changes the commit
that was approved. The signed, provenance-attested workflow-generated exact-SHA
gate-receipt bundle plus the GitHub deployment audit are the authoritative
per-release evidence; this section remains the requirements/current-development
register and later historical index.

### 15.3 Release boundary

The release is a clean major-version boundary. It does not migrate current
`pgpipe.*` safety tables, preserve development repair history or ledger
evidence, support mixed old/new binaries, retain backward-compatible safety
schema names, perform package-driven database migration, delete databases or
volumes automatically, or fall back to legacy/runtime ownership. Existing
disposable demo data is reset only with an explicit `make demo-reset`; native
development data is preserved unless a DBA performs a separately reviewed
reset, with a new destination database and empty configuration/state paths as
the preferred greenfield route.

Repair remains disabled by default and must not be advertised as generally
available until every register row is supported by the exact-candidate evidence
and final approvals required above.
