Prepare compatible audit receiver with verified container lifecycle
All checks were successful
CI Smoke / host-smoke (push) Successful in 1s
CI Smoke / container-smoke (push) Successful in 1s

Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a07ff8-19d0-7820-b4d0-1353833cb7fc
This commit is contained in:
tegwick 2026-09-11 06:46:15 +02:00
parent f1b0577095
commit cb23dc82fd
8 changed files with 426 additions and 17 deletions

View file

@ -1,18 +1,30 @@
# Base pinned by digest, not a mutable tag (AUDIT-WP-0005-T03).
# Same python:3.12-slim digest as the deployed user-engine image.
FROM python:3.12-slim@sha256:d764629ce0ddd8c71fd371e9901efb324a95789d2315a47db7e4d27e78f1b0e9
# Shared Python/Alpine baseline used by the factory approval services.
# Digest and dependency hashes are release inputs; scan the resulting image.
FROM python:3.12-alpine@sha256:b64631e04e4920160c50fbe8d8df828f7f35f06f425cb44aa09bca53e708a35a AS build
ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 PATH=/opt/venv/bin:$PATH
RUN python -m venv /opt/venv
COPY requirements.lock /build/requirements.lock
RUN pip install --no-cache-dir --require-hashes --only-binary=:all: -r /build/requirements.lock
WORKDIR /build
COPY pyproject.toml README.md LICENSE ./
COPY audit_core ./audit_core
RUN pip install --no-cache-dir --no-deps .
FROM python:3.12-alpine@sha256:b64631e04e4920160c50fbe8d8df828f7f35f06f425cb44aa09bca53e708a35a
ARG GIT_COMMIT=unknown
LABEL org.opencontainers.image.title="audit-core" \
org.opencontainers.image.source="https://forgejo.coulomb.social/coulomb/audit-core" \
org.opencontainers.image.revision="${GIT_COMMIT}"
RUN useradd --system --uid 10001 --create-home audit-core
ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 PATH=/opt/venv/bin:$PATH
RUN apk add --no-cache 'libuuid>=2.42.3-r1' \
&& addgroup -S -g 10001 audit-core \
&& adduser -S -u 10001 -G audit-core -H audit-core
COPY --from=build /opt/venv /opt/venv
RUN rm -rf /usr/local/bin/pip* /usr/local/lib/python3.12/site-packages/pip* \
/usr/local/lib/python3.12/site-packages/setuptools* /usr/local/lib/python3.12/site-packages/pkg_resources \
/opt/venv/bin/pip* /opt/venv/lib/python3.12/site-packages/pip* \
/opt/venv/lib/python3.12/site-packages/setuptools* /opt/venv/lib/python3.12/site-packages/pkg_resources
WORKDIR /app
COPY pyproject.toml README.md LICENSE ./
COPY audit_core ./audit_core
RUN pip install --no-cache-dir ".[serve,postgres]"
USER 10001:10001
ENV PYTHONUNBUFFERED=1
EXPOSE 8080
CMD ["audit-core-ingest"]

View file

@ -643,10 +643,21 @@ def serve(app, host: str, port: int, threads: int, timeout: int) -> None:
return
log.info("serving on waitress host=%s port=%s threads=%s", host, port, threads)
waitress_serve(
app, host=host, port=port, threads=threads,
channel_timeout=timeout, ident="audit-core",
)
# PID 1 ignores the default SIGTERM disposition. Raising SystemExit lets
# Waitress stop its dispatcher and wait for active workers (bounded by its
# shutdown timeout). A sender without an acknowledgement must still retry
# the same event id; shutdown never substitutes for durable acceptance.
def terminate(signum, frame):
raise SystemExit(0)
previous = signal.signal(signal.SIGTERM, terminate)
try:
waitress_serve(
app, host=host, port=port, threads=threads,
channel_timeout=timeout, ident="audit-core",
)
finally:
signal.signal(signal.SIGTERM, previous)
def _serve_fallback(app, host: str, port: int, timeout: int) -> None:

View file

@ -0,0 +1,109 @@
{
"status": "release_prepared",
"source_base": "f1b05770956a251da1b9de274f6bd1de5e325b5f",
"receiver_tests": {
"passed": 229,
"command": "PYTHONPATH=../infospace-bench/src AUDIT_CORE_TEST_DATABASE_URL=<disposable-postgres> python -m pytest -q -ra --ignore=tests/test_migrate_store.py",
"excluded": "tests/test_migrate_store.py: five pre-existing failures, one passing empty case. AUDIT-IN-0004 owns repair. No native import."
},
"container": {
"image": "audit-core:hfact-20260911-hardened",
"native_credentials_used": false,
"checks": {
"operational_ready": true,
"nonroot_readonly": true,
"approval-engine": {
"accepted": 202,
"duplicate": 200,
"wrong_source": 400,
"wrong_tenant": 400,
"read_routes_denied": 7,
"independent_read": 200,
"redacted": true,
"stored_once": true
},
"informed-decision": {
"accepted": 202,
"duplicate": 200,
"wrong_source": 400,
"wrong_tenant": 400,
"read_routes_denied": 7,
"independent_read": 200,
"redacted": true,
"stored_once": true
},
"sigterm": {
"exit_code": 0,
"inflight_committed_once": true,
"restart_retry_duplicate": true,
"chain_intact": true
}
},
"started_at": "2026-09-11T04:43:46.088591+00:00",
"status": "passed",
"own_fixture_cleanup": true
},
"schema_comparison": {
"live": {
"0001-events": "e613b4bfb54914fbd98a4abd1fd6017e37108df43ffc306ab825f2d5a8afdd3c",
"0002-append-only": "29251f9300f33cfb90fd40677f5fedbe40057e1c0c866cd29a1d2cb160c4a762",
"0003-dead-letters": "a82024b5b4137aecc7885ac8e1212b1711dd06bc5806a01e79882c4a1092d341",
"0004-secret-findings": "f269be3e19d2f69673b96a65fa33a2b1f863a39dfff6f9937062ab6aa58f63e5",
"0005-runtime-grants": "008247e2daeae5a286cd6e46328fbf0d15fa70c6a99b69cadb296a0c713791fe",
"0006-chain": "a984f8226a5d407f990447110f0dd61e400ef4d9da952a268872e5e75f81db2d"
},
"candidate": {
"0001-events": "e613b4bfb54914fbd98a4abd1fd6017e37108df43ffc306ab825f2d5a8afdd3c",
"0002-append-only": "29251f9300f33cfb90fd40677f5fedbe40057e1c0c866cd29a1d2cb160c4a762",
"0003-dead-letters": "a82024b5b4137aecc7885ac8e1212b1711dd06bc5806a01e79882c4a1092d341",
"0004-secret-findings": "f269be3e19d2f69673b96a65fa33a2b1f863a39dfff6f9937062ab6aa58f63e5",
"0005-runtime-grants": "008247e2daeae5a286cd6e46328fbf0d15fa70c6a99b69cadb296a0c713791fe",
"0006-chain": "a984f8226a5d407f990447110f0dd61e400ef4d9da952a268872e5e75f81db2d"
}
},
"native_before": {
"ready": {
"status": "ok",
"custody_class": "operational",
"durable": true,
"tamper_evidence": true,
"recoverable_days": 30,
"recoverable_source": "resource-control/data/capability/platform-audit-storage.json#provisions[capability=data.backup]",
"recoverable_basis": "measured"
},
"chain": {
"intact": true,
"events": 32,
"head": "9fc0b4f25c562cec037a5077f00ee63380db8b5c4e85b6016fc40b301431e4fd",
"head_event_id": "whitehat-e2-event-b-20260822-03",
"head_accepted_at": "2026-08-22T22:09:37+00:00",
"first_break": null,
"attestation_match": null
},
"image": "forgejo.coulomb.social/coulomb/audit-core@sha256:c2fe39a0185b99be3fc0cb14d2de69772b8e66e20490097c9d11d90cc39719a6",
"generation": 15,
"deployment_uid": "b85fe3d0-75c9-4e0d-8c34-c6f1df0881bb"
},
"scan": {
"candidate": {
"sha256": "33be06088970b025af916a81ad3833313749c2ff25f0fd99641b6fc4823c1ff3",
"vulnerabilities": {
"LOW": 85,
"HIGH": 81,
"MEDIUM": 99,
"UNKNOWN": 6,
"CRITICAL": 3
}
},
"hardened": {
"sha256": "c7598f203067f36be1d0c0f2a7b33c3e02b13583d605d7091baea1bb380f604f",
"vulnerabilities": {}
},
"scanner": "aquasec/trivy@sha256:62b1e65e8869bc4b4c6aa4fa2b21595256c7c2f6018a9d9ad61caf87187c1969",
"warning": "OS lifecycle list does not yet include Alpine 3.24; vulnerability scanner recognized Alpine 3.24 repository."
},
"old_candidate_sigterm_exit": 137,
"native_rollout": "pending",
"factory_attempts": 0,
"paid_model_calls": 0
}

View file

@ -10,8 +10,8 @@ In-cluster URL:
http://audit-core.audit-core.svc.cluster.local:8080
```
Reachability is part of the threat model. Ingress is allowed only from the
`user-engine` namespace (write) and from namespaces labelled
Reachability is part of the threat model. Ingress is allowed from the declared producer peers in
`deploy/networkpolicies.yaml` (write) and from namespaces labelled
`railiance.io/audit-core-reader=true` (operator read). From a workstation,
`kubectl -n audit-core port-forward svc/audit-core 8080:8080` is the usual
path and does not require that label.
@ -261,3 +261,32 @@ narrows a column must replace that note before release.
5. Deployment. `/readyz` must report `custody_class=operational` and
`recoverable_days=30`.
6. In-pod `MODE=remote DISRUPT=0` failure matrix. Evidence goes to NK-WP-0024.
## Factory sender receiver release (2026-09-11)
Build from a committed source with `make image-build`. `requirements.lock` pins
the five runtime packages with hashes; refresh intentionally with
`uv pip compile pyproject.toml --extra serve --extra postgres --generate-hashes --output-file requirements.lock`.
The image uses the factory Python 3.12 Alpine baseline, non-root UID 10001 and
no runtime package installer. Scan every candidate; a clean vulnerability result
is time-bound and not a claim that all future defects are absent.
The release check script uses only a labelled disposable local PostgreSQL
container, synthetic sender identities, and a fresh schema. It proves exact
source/tenant refusal, all seven evidence read routes denied to writers,
independent readback/redaction, duplicate handling, and a database write in
flight during SIGTERM. Waitress has a bounded five-second worker drain. Requests
without an acknowledgement still require idempotent retry; do not promise that
every queued or long-running request finishes inside a shutdown window.
The original c2fe39a image and this release have identical migrations 0001-0006.
This receiver upgrade requires no schema migration job. Rollback to c2fe39a is
structurally compatible, but loses evidence classification support: stop the two
new factory producers first and retain the custody records, then re-admit their
sender contract before resuming. Do not claim load-bearing admission while the
legacy receiver is running.
The historical SQLite importer has five PostgreSQL test failures tracked by
AUDIT-IN-0004. It is outside the live receiver path; do not use it as a recovery
procedure until that record closes. The release test result must state this
exclusion explicitly, never describe the entire suite as green.

View file

@ -177,3 +177,31 @@ resolution: 'Assent. The split is right: approval-engine owns the operative stat
recorded_in: history/2026-08-28-approval-evidence-assent.md
state_hub_intake_id: "01a04d8f-be67-75ed-a221-d50f99dbb78e"
```
## AUDIT-IN-0004 — Repair SQLite import into chained PostgreSQL custody
```yaml
id: AUDIT-IN-0004
kind: intake
title: Repair SQLite import into chained PostgreSQL custody
status: open
origin: residual
origin_ref: AUDIT-WP-0005
priority: medium
owner: audit-core
requested_by: codex
created: '2026-09-11'
updated: '2026-09-11'
description: >
The 2026-09-11 receiver release rehearsal enables real PostgreSQL tests.
Five tests in tests/test_migrate_store.py fail because _import_event omits
chain_hash/chain_prev, now NOT NULL. This predates the receiver release and
is not used by the native PostgreSQL receiver or its unchanged schema.
Design the historic accepted_at versus chain ordering rule before fixing
the importer: do not disable append-only protection or rewrite existing
event timestamps to make tests pass. Cover empty and populated destinations,
concurrency with live append, idempotent replay, conflict preservation, and
an intact chain. Also evaluate repeated dead-letter/finding import counting.
Run the full PostgreSQL suite with no migration exclusions for closure.
No native import is authorized or attempted by this release.
```

77
requirements.lock Normal file
View file

@ -0,0 +1,77 @@
# This file was autogenerated by uv via the following command:
# uv pip compile pyproject.toml --extra serve --extra postgres --generate-hashes --output-file requirements.lock
psycopg==3.3.5 \
--hash=sha256:ce5aa5cdb4f9379f00f487590e5890bfa7df9a164648c969ffa628505e21af4e \
--hash=sha256:d0a3d9ccf5788af054cbd745278cb02401b5c312aeaafbf2c6144460aec47da4
# via audit-core (pyproject.toml)
psycopg-binary==3.3.5 \
--hash=sha256:0249c3e960cdee686000eb77169fb6590105c05bacc37e057ccdffdcd8e6ebde \
--hash=sha256:04f64b39830887c2c737b522cbfd6ad215d65e67ebfff674aa4cf21c02af487b \
--hash=sha256:06de14ac978a2d53e864069fb5487075c6e3cfb0740f1bfd7017bc8b9942067f \
--hash=sha256:0d8a4b7ae47f3381e2ded89891d2455b809f4afb7e5b58086844abb8cfa420ea \
--hash=sha256:1344fd57a19737554670e67aecabd4fb37cd7937f2925840009645d641117e5a \
--hash=sha256:14f432430fd9e1a9e7d9ab2fe14956c77f5d074ebdc556a1ad04e9a1bd3fca04 \
--hash=sha256:14fdfd65a96ecbd8b586d14546105641f4a6ac7cbe335c786830ea4de94bbe60 \
--hash=sha256:19e5bf9872dbd164c220567fd385ba2309c7d9df1541f78343510c6b0f36a1b7 \
--hash=sha256:1ef2e498be47800f6202b9a2304c22646325ca6d54001b7c785bcfdb24a1e8ab \
--hash=sha256:2111f880add40fb03c60556069ad68e884a0908a74d2debafc603caf93b73552 \
--hash=sha256:25105f9b46bdf2a30fcb67f56976ed66f6855941ae16bc024192609b917d493c \
--hash=sha256:2719fe19a4da752c4110cc767716d0a5bdb760d1153d89018bb7c9c61717bde5 \
--hash=sha256:358748fc4c8ccdc0e2bdf55420494930e19c3ade586ea9c3a6de3dad1f897311 \
--hash=sha256:35885e333020fc152d27bea1a494bef13b2e68f6fd92b6229015e93539152008 \
--hash=sha256:39e70c8e3b5fad70e2970ea4cc502bf3b612018f128aa1c670f1fa78b9774543 \
--hash=sha256:40505676b1526b9ea387dace034040a8c8b0bcf984cd6bd4720a2ab15e813586 \
--hash=sha256:40f8b132c7243ef5f503f0b6f986bf16d38a51b0df1c6ba2577743f128be03e3 \
--hash=sha256:479b96fd78149cfa10369dc53fbfb89ee729be13146b584a23dbc7e164c0cf1e \
--hash=sha256:4901e5b9a31c230211a1871263b6594373bacd770b14ad9b14d349716cb69cbe \
--hash=sha256:553b5443cbc94fdb9b0e31b62acdf615e0780982d6b13752a15eb3d6c0dfd0d2 \
--hash=sha256:5698ab5941a4d138c30fef858588e651fe7d583280cd6e41832825ad9e747750 \
--hash=sha256:5816472e3bb05615f33a741e0835043d1f4bf9709ff30d2f4aed71815cfc6b5e \
--hash=sha256:5b981d25fc2dd13fa7328e40703ea8a03f3e9d855ec946431e96a23206d3b9fd \
--hash=sha256:682a17a57415c3ca1731eec018ed031f012ffcb81ba74806eb219cb396065672 \
--hash=sha256:6e85d50b87257fb117675a19ee59daa7bf9a57f6431500adf7059df799232ef4 \
--hash=sha256:7b443f943abfe35aa5a776630cea27c9348aa66659286cee0b99084332252080 \
--hash=sha256:88e01aa2e938a45655a8a5213fc3a44ba78cb4cab8a569b3e0bcb3d1d0eaba16 \
--hash=sha256:893ce86a4b997f6ca1261a7826db2506727332a6ff66646fa7f024b39b5e630e \
--hash=sha256:8dbd694f3741dd4ac5bc60b70e17f7841aefb3f0f38cef4d2756de270e03af43 \
--hash=sha256:972cc28e943746e71ede254a4dfd1fdfcdd6dcadd6f375703849859f09377f24 \
--hash=sha256:98a388509306e5e08a4203253ac52846bc1b034e5cbd0ae6da1211593cc28594 \
--hash=sha256:9c071bf78e5c2e6efa40bc9089a954d7b41221347a72f35c6bf2d8c96e632f75 \
--hash=sha256:a5e45e4bb68656253ce5c7a344c0a425c293581eba954d7fd7c2e4b2dc9f3038 \
--hash=sha256:ab39e2794b95af61a2ff69e33e5ab6ac5df36e9ffea9a3b18e38b2aaca8c5ad5 \
--hash=sha256:ae67072db949d0c094b747a8ec52ad0fa3c42b27842a5f746f3613d54dde3fba \
--hash=sha256:af5084124fb2fd16557073822519dfe8c389636a16adef661a4c0c3918733171 \
--hash=sha256:ba466011569297114449df9d523438e1adeedf3e4f31ffb78e897ec3fef3076b \
--hash=sha256:c065531e8c1815276f50dbfa283e3a7f022671414cdda6fa9a16794dd53b28f9 \
--hash=sha256:c09775c549b40b274206e1b043c5e5b5af39666e85c98382a30bd05d23ab677b \
--hash=sha256:c0cac998b9b1e82dec853d2e53b3d34d56a525cf231f9441a636cfd5992929a9 \
--hash=sha256:c6bd84e4cf67930f26f015dec33f615472b9c5871d46408efe112dbc1bc021de \
--hash=sha256:ca8af7c0454cdce235d4aedcb5528857468f1490202d25e24fc7af40e176d563 \
--hash=sha256:cb3b3bffebfe07110730626e76238161124f35ac87b748d663316a28d22f58b0 \
--hash=sha256:cd0faa2475ab254ad1b507430131cf7f7f0be927ffdc03c32ad3b33d2ef63f42 \
--hash=sha256:cf0e5e63ee86098299c673992053d556c489ba9ae6aca6cb6e24d16a8e0b09e6 \
--hash=sha256:d06da67e9c687c6a6fdac9da4b17cbeb296ddd59bd01f6416ed4294bc57c5faf \
--hash=sha256:d2a61e8147902771df7efe14062a3c8736347850d0d8befcf048235752504f2e \
--hash=sha256:d8b66353b20e79bf7ac0a80f03ae97f522ccbbf909d687eec62f112e56c0276d \
--hash=sha256:df209e64674a34b41662c67fdc8b4e0ffd77d2136393790691d086a09f9a6cab \
--hash=sha256:df9853b832b7b916e02ef68e0d5403a7dab2d5c1ddfe94f22b1b155eb862622f \
--hash=sha256:e5becd311f9af8d180bad372f51fb2252fd02cb2073056e2b170c9274f95fe7f \
--hash=sha256:f45d77e398542ce0937d9fa3cd9d84e9c5fc6b34c50a66404ae840bada312750 \
--hash=sha256:f7e1e45aad410e20de45df2b159df68ff6c8dbf47a3501f806c4489b27f4ad2b \
--hash=sha256:fd5b047c9fd887b767d063845413e405f5de8ce1dc7a9d0da0637b77b836b469 \
--hash=sha256:fdbeb38c9b7ca8fa57a7bda3802bedb62f4494ad3dd46c7dd36dc3f77fd5093f
# via psycopg
psycopg-pool==3.3.1 \
--hash=sha256:2af5b432941c4c9ad5c87b3fa410aec910ec8f7c122855897983a06c45f2e4b5 \
--hash=sha256:b10b10b7a175d5cc1592147dc5b7eec8a9e0834eb3ed2c4a92c858e2f51eb63c
# via psycopg
typing-extensions==4.16.0 \
--hash=sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8 \
--hash=sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5
# via
# psycopg
# psycopg-pool
waitress==3.0.2 \
--hash=sha256:682aaaf2af0c44ada4abfb70ded36393f0e307f4ab9456a215ce0020baefc31f \
--hash=sha256:c56d67fd6e87c2ee598b76abdd4e96cfad1f24cacdea5078d382b1f9d7b5ed2e
# via audit-core (pyproject.toml)

View file

@ -0,0 +1,127 @@
#!/usr/bin/env python3
"""Exercise a candidate against a labelled disposable local PostgreSQL container.
No native credentials or cluster access. Requires psycopg on the host. Keeps
only a JSON receipt; deletes only the receiver and database schema it creates.
"""
import argparse
from concurrent.futures import ThreadPoolExecutor
from datetime import datetime, timezone
import json
from pathlib import Path
import subprocess
import time
import uuid
import psycopg
def run(argv, **kwargs):
return subprocess.run(argv, check=True, capture_output=True, text=True, **kwargs).stdout
def main():
p = argparse.ArgumentParser(description=__doc__)
p.add_argument('--image', required=True)
p.add_argument('--postgres-container', required=True)
p.add_argument('--postgres-port', type=int, required=True)
p.add_argument('--receipt', type=Path, required=True)
a = p.parse_args()
pg = json.loads(run(['docker', 'inspect', a.postgres_container]))[0]
assert pg['Config']['Labels'].get('hfact.task') == 'audit-release-20260911', 'local test fixture required'
suffix = uuid.uuid4().hex[:12]
schema = 'release_' + suffix
name = 'audit-release-check-' + suffix
dsn = f'postgresql://postgres:local-test-only@127.0.0.1:{a.postgres_port}/audit_core'
registry = [{'name': n, 'tokens': [n+'-fixture'], 'sources': [n],
'tenants': ['tenant:platform'], 'may_write': True, 'may_read': False,
'evidence_kind': 'load-bearing', 'secret_policy': 'redact'}
for n in ['approval-engine', 'informed-decision']]
registry.append({'name':'independent-reader','tokens':['reader-fixture'],'sources':['*'],
'tenants':['*'],'may_write':False,'may_read':True})
proof = {'image': a.image, 'native_credentials_used': False, 'checks': {},
'started_at': datetime.now(timezone.utc).isoformat()}
created = False
def request(method, path, token='', body=None):
# Values here are local synthetic fixtures, never native credentials.
payload=json.dumps({'method':method,'path':path,'token':token,'body':body})
code='''import json,sys,urllib.request,urllib.error
x=json.load(sys.stdin); b=x['body']
r=urllib.request.Request('http://127.0.0.1:8080'+x['path'],method=x['method'],data=json.dumps(b).encode() if b is not None else None,headers={'Content-Type':'application/json','Authorization':'Bearer '+x['token'],'Idempotency-Key':b['id'] if b else ''})
try:
with urllib.request.urlopen(r,timeout=15) as f: print(json.dumps([f.status,json.load(f)]))
except urllib.error.HTTPError as e: print(json.dumps([e.code,json.load(e)]))
'''
return json.loads(run(['docker','exec','-i',name,'python','-c',code],input=payload))
def event(sender, suffix):
return {'id':suffix,'source':sender,'type':'factory.custody-check',
'subject':'synthetic-release-check','tenant':'tenant:platform','correlation_id':'release-'+suffix,
'occurred_at':datetime.now(timezone.utc).isoformat(),
'data':{'synthetic':True,'auth_token':'fixture-must-be-redacted'}}
try:
run(['docker','run','-d','--name',name,'--network','container:'+a.postgres_container,
'--read-only','--tmpfs','/tmp','--cap-drop','ALL','--security-opt','no-new-privileges',
'-e','AUDIT_CORE_DATABASE_URL=postgresql://postgres:local-test-only@127.0.0.1:5432/audit_core',
'-e','AUDIT_CORE_DATABASE_SCHEMA='+schema,'-e','AUDIT_CORE_REQUIRE_CUSTODY_CLASS=operational',
'-e','AUDIT_CORE_SENDERS='+json.dumps(registry),a.image])
created = True
for _ in range(50):
try:
status,body=request('GET','/readyz')
if status==200: break
except (subprocess.CalledProcessError, ValueError): time.sleep(.2)
else: raise AssertionError('receiver readiness timed out')
proof['checks']['operational_ready']=body['custody_class']=='operational'
assert proof['checks']['operational_ready']
uid=run(['docker','exec',name,'id','-u']).strip(); assert uid=='10001'
proof['checks']['nonroot_readonly']=True
for sender in ['approval-engine','informed-decision']:
e=event(sender,sender+'-'+suffix); token=sender+'-fixture'
assert request('POST','/v1/events',token,e)[0]==202
assert request('POST','/v1/events',token,e)[0]==200
assert request('POST','/v1/events',token,dict(e,source='sibling'))[0]==400
assert request('POST','/v1/events',token,dict(e,tenant='tenant:sibling'))[0]==400
for path in ['/v1/events/'+e['id'],'/v1/events?correlation_id=fixture','/v1/stats','/v1/integrity','/v1/dead-letters','/v1/secret-findings','/v1/stream-findings']:
assert request('GET',path,token)[0]==403, path
status,stored=request('GET','/v1/events/'+e['id'],'reader-fixture')
assert status==200 and 'fixture-must-be-redacted' not in json.dumps(stored)
with psycopg.connect(dsn) as c:
assert c.execute(f'SELECT count(*) FROM {schema}.events WHERE event_id=%s',(e['id'],)).fetchone()[0]==1
proof['checks'][sender]={'accepted':202,'duplicate':200,'wrong_source':400,'wrong_tenant':400,'read_routes_denied':7,'independent_read':200,'redacted':True,'stored_once':True}
e=event('approval-engine','shutdown-'+suffix)
with psycopg.connect(dsn,autocommit=True) as lock, ThreadPoolExecutor() as pool:
lock.execute('SELECT pg_advisory_lock(%s)',(0xA0D17007,))
pending=pool.submit(request,'POST','/v1/events','approval-engine-fixture',e)
for _ in range(100):
if lock.execute("SELECT count(*) FROM pg_locks WHERE locktype='advisory' AND NOT granted").fetchone()[0]: break
time.sleep(.05)
else: raise AssertionError('in-flight database write not observed')
stop=pool.submit(run,['docker','stop','--timeout','10',name])
time.sleep(.5)
lock.execute('SELECT pg_advisory_unlock(%s)',(0xA0D17007,))
try: pending.result()
except subprocess.CalledProcessError: pass # Lost acknowledgement requires retry.
stop.result()
code=int(run(['docker','inspect',name,'--format','{{.State.ExitCode}}']).strip()); assert code==0, code
with psycopg.connect(dsn) as c:
assert c.execute(f'SELECT count(*) FROM {schema}.events WHERE event_id=%s',(e['id'],)).fetchone()[0]==1
run(['docker','start',name])
for _ in range(50):
try:
if request('GET','/readyz')[0]==200: break
except subprocess.CalledProcessError: pass
time.sleep(.2)
assert request('POST','/v1/events','approval-engine-fixture',e)[0]==200
status,integrity=request('GET','/v1/integrity','reader-fixture')
assert status==200 and integrity['intact'] and integrity['events']==3
proof['checks']['sigterm']={'exit_code':code,'inflight_committed_once':True,'restart_retry_duplicate':True,'chain_intact':True}
proof['status']='passed'
finally:
if created: run(['docker','rm','-f',name])
with psycopg.connect(dsn) as c: c.execute(f'DROP SCHEMA IF EXISTS {schema} CASCADE')
proof['own_fixture_cleanup']=True
with a.receipt.open('x') as f: json.dump(proof,f,indent=2); f.write('\n')
print(json.dumps(proof,indent=2))
if __name__=='__main__': main()

View file

@ -456,8 +456,8 @@ CCR-2026-0021 (approval-engine) and CCR-2026-0022 (informed-decision), with
exact-path ESO projections and CAS-preserving first provision/resume. Its
receiver check refuses the current image before credential access. Seventeen
new tests, including eleven local OpenBao cases, and 53 existing credential-change
tests pass. Named owner reviews, compatible receiver release and target namespace
readiness remain prerequisites. No native sender credential or deployment changed.
tests pass. The user approved both named owner review sets on 2026-09-11.
Compatible receiver release and target namespace readiness remain prerequisites.
See railiance-platform/docs/credential-lane-designs/factory-audit-senders-review.md
and its dated preparation evidence. Existing sender/source scopes are preserved.
@ -483,3 +483,19 @@ to cover.
T05's dependency is real and should not be worked around. audit-core argued the
cadence obligation up to a MUST; inventing a local cadence shape ahead of the
Taxonomy schema would fragment the very interface it asked for.
### Compatible receiver release rehearsal — 2026-09-11
229 receiver/contract tests pass with real disposable PostgreSQL. The full suite
exposed five historical SQLite-import failures, now handed off as AUDIT-IN-0004;
that importer is not used by the native receiver. No migration SQL changed.
The new image uses the factory Alpine baseline and hashed runtime dependencies.
Candidate scan went from 81 High/3 Critical to zero reported vulnerabilities,
with the Alpine lifecycle-list warning retained. SIGTERM previously killed
PID 1 with exit 137; explicit handling now exits 0, preserves one in-flight
write and returns duplicate on restart/retry. Both synthetic senders pass exact
scope, redaction, independent readback and seven read-route denials.
[Rehearsal evidence](../docs/evidence/2026-09-11-factory-receiver-release.json).
Publish and native rollout are the next steps; T09/T11 remain progress until
real sender custody, delivery and owner acceptance are evidenced.