# Ephemeral custody projection lifecycle

`scripts/custody-projection.py` is the platform-owned interface for future
short-lived, mount-only credential exercises. It replaces copying the dated
`audit-core-whitehat-e2-credentials*.py`, HCL, and YAML files. Those dated
artifacts are retained only as historical evidence and cleanup references for
their terminal engagement ids.

The lifecycle does not authorize an engagement. A projection contract is
accepted only when it is `approved`, lasts at most 900 seconds, binds exact
target and runner revisions, names exactly two distinct sender identities, and
uses engagement- and target-bound KV paths. Kubernetes and OpenBao resource
names are derived from the SHA-256 of the engagement id; no previous engagement
name is copied into a new manifest.

## Versioned interfaces

| Interface | Schema | Producer | Consumer |
| --- | --- | --- | --- |
| `railiance.custody-projection-contract` v1 | `schemas/custody-projection-contract.schema.json` | engagement/platform owners | platform lifecycle and Whitehat review |
| `railiance.custody-broker-readiness` v1 | `schemas/custody-broker-readiness.schema.json` | `whitehat-security` | platform pre-projection gate |
| `railiance.custody-projection-receipt` v1 | `schemas/custody-projection-receipt.schema.json` | platform lifecycle | Whitehat broker adapter and operator |
| `railiance.custody-cleanup-receipt` v1 | `schemas/custody-cleanup-receipt.schema.json` | platform lifecycle | engagement owner and evidence record |

Receipts contain no bearer, password, value-derived fingerprint, Secret data,
or registry body. The projection receipt carries an opaque lease id, canonical
receipt id, exact identity roles and mount paths, contract digests, times,
resource names, and Kubernetes UIDs. Changing any canonical field invalidates
the receipt id.

## Broker gate owned directly by Whitehat

The owner does not need a coding agent to translate the request. Given the
final projection contract, it can run:

```sh
python3 scripts/wp0025-broker-readiness.py show \
  --contract /path/to/final-projection-contract.json

python3 scripts/wp0025-broker-readiness.py verify \
  --contract /path/to/final-projection-contract.json \
  --consumer-root /path/to/whitehat-security

python3 scripts/wp0025-broker-readiness.py approve \
  --contract /path/to/final-projection-contract.json \
  --consumer-root /path/to/whitehat-security \
  --reviewer whitehat-owner
```

Verification is closed to
`src/whitehat_security/platform_custody.py` and
`tests/test_platform_custody_adapter.py`. It records the full consumer commit,
adapter SHA-256, focused-test result, exact platform schema hashes, mount paths,
and cleanup support. Approval posts the canonical receipt directly to State
Hub as `from_agent=whitehat-security`. A stale schema, changed projection
contract, wrong engagement, wrong target, absent adapter, or failing focused
test does not count.

The platform lifecycle queries that receipt before token generation and before
the first mutation. State Hub coordination identity is not a secret transport;
the receipt contains no secret.

## Platform commands

The template at `docs/custody-projection-contract.example.json` intentionally
fails validation until every placeholder and the approval status are replaced
by the owners.

```sh
python3 scripts/custody-projection.py validate \
  --contract /path/to/final-projection-contract.json

python3 scripts/custody-projection.py render \
  --contract /path/to/final-projection-contract.json

python3 scripts/custody-projection.py preflight \
  --contract /path/to/final-projection-contract.json
```

`render` is value-free and creates no resource. `preflight` reads only status,
metadata, names, image/readiness, and the broker receipt. It never reads a
Kubernetes Secret value.

Inside the approved projection interval, an attached custody operator may run:

```sh
python3 scripts/custody-projection.py project \
  --contract /path/to/final-projection-contract.json \
  --confirm '<engagement-id>:attended' \
  --receipt-out /approved/non-repo/path/projection.json
```

If any mutation fails, the transaction invokes exact-scope cleanup. The
operator must not release the consumer until the returned projection receipt
passes its adapter. The receipt file is written mode `0600` even though it is
value-safe.

Status and normal attended cleanup require that receipt:

```sh
python3 scripts/custody-projection.py status \
  --contract /path/to/final-projection-contract.json \
  --receipt /approved/non-repo/path/projection.json

python3 scripts/custody-projection.py cleanup \
  --contract /path/to/final-projection-contract.json \
  --receipt /approved/non-repo/path/projection.json \
  --confirm '<engagement-id>:cleanup' \
  --receipt-out /approved/non-repo/path/cleanup.json
```

Status reports `active`, `absent`, `partial`, or `mismatched`. It compares live
resource UIDs with the receipt and does not interpret a connection error as
absence.

## Expired cleanup entry point

`cleanup-expired` refuses until the contract expiry and still requires the
exact projection receipt and confirmation:

```sh
python3 scripts/custody-projection.py cleanup-expired \
  --contract /path/to/final-projection-contract.json \
  --receipt /approved/non-repo/path/projection.json \
  --confirm '<engagement-id>:expired' \
  --receipt-out /approved/non-repo/path/cleanup.json
```

This is an approval-ready reaper entry point, not a deployed scheduler. A
future controller still needs an owner-reviewed credential route, runtime
identity, alerting, and rollout decision. Application `expires_at` remains the
acceptance backstop; evidenced resource deletion remains the cleanup result.
