Publish and pin the verified schema-v5 approval candidate

Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a07ff8-19d0-7820-b4d0-1353833cb7fc
This commit is contained in:
tegwick 2026-09-10 19:35:05 +02:00
parent be1a388a84
commit c8f85c6d76
6 changed files with 172 additions and 33 deletions

View file

@ -5,24 +5,20 @@ deployment. SQLite is intentionally limited to one replica, `ReadWriteOnce`
storage, and an `OnDelete` update: never start two writers against a copied
database.
**Image: pinned, published, not rolled out.** Both `image:` references already
carry the immutable release digest
`sha256:73333f5ceb55e48192e3095cb2e2a741cdc6ff0be2f18128301072b4a6b6eb9d`
(registry tag `0.1.0`, OCI index; linux/amd64 manifest `sha256:2064d537…`),
pinned at `b51d174`. There is no placeholder left to replace. Both references
MUST stay identical and MUST stay digests — a tag here would let the init
container and the server run different code against one database.
**Image: pinned, published, not rolled out.** Both `image:` references carry
`sha256:251941a5cb2724b57cc32cff6b693b1ab0be695bee4f56f02d51961189c0fa49`
(registry tag `0.1.0-hfact-be1a388`, source `be1a388`). Both references MUST stay
identical immutable digests: migration and server share one database.
**The pinned image predates schema v5.** The published artifact recorded in
[`docs/image-scan-2026-09-06.md`](../docs/image-scan-2026-09-06.md) carries
`LATEST_SCHEMA_VERSION = 3`; this repository is now at 5
(`entries.principal_type` plus `approvals.human_control`, see [storage-operations.md](../docs/storage-operations.md)).
The pinned pair is self-consistent — that image migrates to 3 and serves 3 — so
nothing is broken by leaving it pinned, but a rollout that must enforce declared human controls or carry approver
principal-type evidence requires cutting a new image at step 3 below. The
`migrate` init container then performs the additive upgrade on the existing
volume; `tests/test_deploy_manifest.py` holds this acknowledgement so the drift
cannot go quiet.
The new artifact carries `LATEST_SCHEMA_VERSION = 5`, matching this source:
`entries.principal_type` and explicit `approvals.human_control`. Its release scan
found zero HIGH/CRITICAL vulnerabilities. Disposable on-disk v4-to-v5 migration,
negative service/agent binding, human binding/consumption and restart persistence
passed inside the non-root container with no network and a read-only root.
See [the image evidence](../docs/evidence/2026-09-10-human-control-image.json).
This is artifact evidence; native identity, audit delivery and production restore
remain deployment gates. Never roll the old v3 image over a v5 database; use the
matching verified pre-migration backup for rollback.
Gates 1 and 2 below are the outstanding ones; nothing is deployed today.

View file

@ -42,7 +42,7 @@ spec:
seccompProfile: {type: RuntimeDefault}
initContainers:
- name: migrate
image: forgejo.coulomb.social/coulomb/approval-engine@sha256:73333f5ceb55e48192e3095cb2e2a741cdc6ff0be2f18128301072b4a6b6eb9d
image: forgejo.coulomb.social/coulomb/approval-engine@sha256:251941a5cb2724b57cc32cff6b693b1ab0be695bee4f56f02d51961189c0fa49
args: ["migrate", "--db", "/data/approvals.sqlite"]
securityContext:
allowPrivilegeEscalation: false
@ -53,7 +53,7 @@ spec:
- {name: tmp, mountPath: /tmp}
containers:
- name: approval-engine
image: forgejo.coulomb.social/coulomb/approval-engine@sha256:73333f5ceb55e48192e3095cb2e2a741cdc6ff0be2f18128301072b4a6b6eb9d
image: forgejo.coulomb.social/coulomb/approval-engine@sha256:251941a5cb2724b57cc32cff6b693b1ab0be695bee4f56f02d51961189c0fa49
args:
- serve
- --production

View file

@ -0,0 +1,59 @@
import hashlib
import importlib.metadata
import json
import os
import pathlib
import tempfile
from datetime import datetime, timedelta, timezone
import approval_engine.store as store
from approval_engine.errors import Forbidden
assert os.getuid() == 10001
assert store.LATEST_SCHEMA_VERSION == 5
now = datetime.now(timezone.utc)
binding = dict(action='fixture.consume', target={'id': 'fixture'}, actor='service:fixture',
principal='human:fixture', purpose='disposable-image-verification')
validity = {'not_before': (now-timedelta(minutes=1)).isoformat(),
'expires_at': (now+timedelta(minutes=5)).isoformat()}
with tempfile.TemporaryDirectory() as directory:
path = pathlib.Path(directory)/'approvals.sqlite'
engine = store.Engine(path)
legacy = engine.create(binding, validity)
engine.add_entry(legacy.id, 'human:legacy', principal_type='human')
engine._conn().execute('ALTER TABLE approvals DROP COLUMN human_control')
engine._conn().execute('PRAGMA user_version=4')
engine._conn().commit()
engine.close()
engine = store.Engine(path)
assert engine.storage_status()['schema_version'] == 5
assert engine.get(legacy.id).human_control is False
assert engine.claim(legacy.id)['valid_now'] is True
controlled = engine.create(binding, validity, human_control=True)
for principal_type in ('service', 'agent', None):
try:
engine.add_entry(controlled.id, 'nonhuman', principal_type=principal_type)
except Forbidden:
pass
else:
raise AssertionError('nonhuman binding admitted')
assert engine.get(controlled.id).entries == []
engine.add_entry(controlled.id, 'human:fixture', principal_type='human')
assert engine.claim(controlled.id)['binding']['human_control'] is True
assert engine.claim(controlled.id)['valid_now'] is True
engine.consume(controlled.id, controlled.binding_digest)
assert engine.get(controlled.id).status == 'consumed'
engine.close()
reopened = store.Engine(path)
assert reopened.get(controlled.id).human_control is True
assert reopened.get(controlled.id).status == 'consumed'
reopened.close()
print(json.dumps({
'status': 'passed', 'uid': os.getuid(), 'schema_version': 5,
'store_sha256': hashlib.sha256(pathlib.Path(store.__file__).read_bytes()).hexdigest(),
'checks': ['nonroot', 'v4-to-v5-persistent-migration', 'legacy-declaration-false',
'service-agent-unknown-refusal', 'human-bind-claim-consume', 'restart-persistence'],
'native_identity_or_credential_calls': 0, 'network': 'none',
'packages': {name: importlib.metadata.version(name) for name in
('approval-engine', 'PyJWT', 'cryptography', 'waitress')},
}, indent=2))

View file

@ -0,0 +1,69 @@
{
"status": "published-not-deployed",
"source_commit": "be1a388a848cf59e9ff3a5420f88a70ca0a52e7e",
"image": {
"repository": "forgejo.coulomb.social/coulomb/approval-engine",
"tag": "0.1.0-hfact-be1a388",
"digest": "sha256:251941a5cb2724b57cc32cff6b693b1ab0be695bee4f56f02d51961189c0fa49",
"linux_amd64_manifest": "sha256:a9fdac46b7a86f7d50389d551731b3dea321f47e90be66bfb8c3e498a8a544c2",
"schema_version": 5
},
"scan": {
"scanner_image": "sha256:62b1e65e8869bc4b4c6aa4fa2b21595256c7c2f6018a9d9ad61caf87187c1969",
"gate": "CRITICAL,HIGH; exit-code 1",
"exit_code": 0,
"high": 0,
"critical": 0,
"os": "Alpine 3.24.1",
"warning": "Scanner does not list Alpine 3.24 in its EOL list; vulnerability detection ran.",
"lower_severities": "not selected by the existing release gate",
"observed_at": "2026-09-10T17:32:22Z"
},
"container_verification": {
"status": "passed",
"uid": 10001,
"schema_version": 5,
"store_sha256": "eb33ce55f1475e19ab25d0727bc244ffc7375ff9ae5f441d7d9a36fb293d7446",
"checks": [
"nonroot",
"v4-to-v5-persistent-migration",
"legacy-declaration-false",
"service-agent-unknown-refusal",
"human-bind-claim-consume",
"restart-persistence"
],
"native_identity_or_credential_calls": 0,
"network": "none",
"packages": {
"approval-engine": "0.1.0",
"PyJWT": "2.13.0",
"cryptography": "50.0.1",
"waitress": "3.0.2"
}
},
"source_suite": {
"passed": 152,
"new_regressions": 22
},
"production_deployed": false,
"factory_attempts": 0,
"paid_model_calls": 0,
"remaining_owner_records": [
"APPROVAL-WP-0002-T01/T03/T05",
"SECRETS-WP-0009-T03",
"INFD-WP-0001-T07/T08",
"AUDIT-WP-0009-T04/T06/T09"
],
"log_sha256": {
"image-build.txt": "ec1b54fedaccf618f9fe31df5c142d6db4d9612effd900c05c2134eea97a680b",
"image-scan.txt": "e0024173a72bc9d3862ed476aefcf2224a4bd54612c3c1c6e5bb20777aa2d5d1",
"image-push.txt": "c89bb84760614e782eab64e33c72a578f69615adf5fe8ba35778c733975f0551",
"registry-inspect.txt": "577d321cae9c55ed7a7814b33894ecacaf066e4a7b13cb54c488b72938bae1fb",
"release-tests.txt": "48ac9c02d5e186c834889c99c651827dcc7605e674811ba7c6522c9890d7371f",
"deploy-dry-run.txt": "05e28400d629b0aaefad24c7f12d894ca77eb538427eb1a12c7e0e8bce5d8f32"
},
"registry_manifest_verified": true,
"deployment_validation": "kubectl apply --dry-run=client: passed (6 resources); no apply",
"verification_script": "docs/evidence/2026-09-10-human-control-image-smoke.py",
"verification_command": "docker run --rm -i --network none --read-only --tmpfs /tmp:rw,nosuid,nodev --cap-drop ALL --security-opt no-new-privileges --entrypoint python forgejo.coulomb.social/coulomb/approval-engine@sha256:251941a5cb2724b57cc32cff6b693b1ab0be695bee4f56f02d51961189c0fa49 - < docs/evidence/2026-09-10-human-control-image-smoke.py"
}

View file

@ -8,6 +8,7 @@ kind of property a test can hold and prose cannot.
from __future__ import annotations
import json
import re
from pathlib import Path
@ -15,7 +16,7 @@ from approval_engine.store import LATEST_SCHEMA_VERSION
ROOT = Path(__file__).resolve().parents[1]
MANIFEST = ROOT / "deploy" / "approval-engine.yaml"
RELEASE_RECORD = ROOT / "docs" / "image-scan-2026-09-06.md"
RELEASE_RECORD = ROOT / "docs" / "evidence" / "2026-09-10-human-control-image.json"
DEPLOY_README = ROOT / "deploy" / "README.md"
IMAGE_LINE = re.compile(r"^\s*image:\s*(\S+)\s*$", re.MULTILINE)
@ -78,28 +79,23 @@ def test_pinned_image_schema_drift_is_acknowledged():
The checks above hold that migrate and serve run the *same* code. They
cannot see that both run *old* code: a v3 image migrating to 3 and serving
3 is perfectly self-consistent while this repository has moved to 4. That
3 is perfectly self-consistent while this repository has moved to 5. That
reads as healthy and is the more dangerous shape, because the failure is an
assumption ("the deployment records principal_type") rather than an error.
So when the manifest still pins the artifact the release record describes,
the record's schema version must either match this repository or the
runbook must say plainly that the pin is behind. Prose alone loses that;
The release record must describe the exact pinned artifact and its verified
schema. That version must either match this repository or the runbook must
say plainly that the pin is behind. Prose alone loses that;
the test makes stating it the cheaper option.
"""
refs = image_refs()
assert refs, "no image references found"
pinned = refs[0].split("@", 1)[-1]
record = RELEASE_RECORD.read_text(encoding="utf-8")
if pinned not in record:
return # a newer artifact is pinned; this record no longer describes it
versions = {
int(v) for v in re.findall(r"LATEST_SCHEMA_VERSION\s*=\s*(\d+)", record)
}
assert len(versions) == 1, f"release record states {versions or 'no'} schema versions"
recorded = versions.pop()
record = json.loads(RELEASE_RECORD.read_text(encoding="utf-8"))
assert pinned == record["image"]["digest"], "release evidence must describe the pinned image"
recorded = record["image"]["schema_version"]
assert record["container_verification"]["schema_version"] == recorded
if recorded == LATEST_SCHEMA_VERSION:
return

View file

@ -458,6 +458,25 @@ the v4 section (additive `ALTER TABLE`, legacy entries stay `NULL` and are never
back-filled to `human`, downgrade unsupported). T03 stays `wait`: gates 1 and 2
are unchanged and nothing is deployed. 125 tests pass.
### Schema-v5 release candidate — 2026-09-10
The earlier schema drift is resolved in the published candidate, source
`be1a388a848cf59e9ff3a5420f88a70ca0a52e7e`, registry tag
`0.1.0-hfact-be1a388`, immutable digest
`sha256:251941a5cb2724b57cc32cff6b693b1ab0be695bee4f56f02d51961189c0fa49`.
Both migration and serving references now pin it. The sanctioned HIGH/CRITICAL
scan passes with zero findings. Disposable container checks prove schema v5,
v4 migration without inferred human declaration, nonhuman bind refusal,
human bind/claim/consume and restart persistence; packaged store bytes match
committed source. These checks use synthetic local identities and no network.
See `docs/evidence/2026-09-10-human-control-image.json` for the exact evidence.
T03 remains `wait`: native KeyCape requester/approver registration and audit
sender custody still precede rollout, live persistence/outbox/restore proof.
The factory requester and PEP must explicitly declare/require human_control in
T05 with SECRETS-WP-0009-T03 and INFD-WP-0001-T08. No production database was
migrated, no workload deployed and no factory/model request admitted.
## Wire outbox delivery and reconciliation
```task