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

View file

@ -42,7 +42,7 @@ spec:
seccompProfile: {type: RuntimeDefault} seccompProfile: {type: RuntimeDefault}
initContainers: initContainers:
- name: migrate - 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"] args: ["migrate", "--db", "/data/approvals.sqlite"]
securityContext: securityContext:
allowPrivilegeEscalation: false allowPrivilegeEscalation: false
@ -53,7 +53,7 @@ spec:
- {name: tmp, mountPath: /tmp} - {name: tmp, mountPath: /tmp}
containers: containers:
- name: approval-engine - name: approval-engine
image: forgejo.coulomb.social/coulomb/approval-engine@sha256:73333f5ceb55e48192e3095cb2e2a741cdc6ff0be2f18128301072b4a6b6eb9d image: forgejo.coulomb.social/coulomb/approval-engine@sha256:251941a5cb2724b57cc32cff6b693b1ab0be695bee4f56f02d51961189c0fa49
args: args:
- serve - serve
- --production - --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 from __future__ import annotations
import json
import re import re
from pathlib import Path from pathlib import Path
@ -15,7 +16,7 @@ from approval_engine.store import LATEST_SCHEMA_VERSION
ROOT = Path(__file__).resolve().parents[1] ROOT = Path(__file__).resolve().parents[1]
MANIFEST = ROOT / "deploy" / "approval-engine.yaml" 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" DEPLOY_README = ROOT / "deploy" / "README.md"
IMAGE_LINE = re.compile(r"^\s*image:\s*(\S+)\s*$", re.MULTILINE) 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 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 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 reads as healthy and is the more dangerous shape, because the failure is an
assumption ("the deployment records principal_type") rather than an error. assumption ("the deployment records principal_type") rather than an error.
So when the manifest still pins the artifact the release record describes, The release record must describe the exact pinned artifact and its verified
the record's schema version must either match this repository or the schema. That version must either match this repository or the runbook must
runbook must say plainly that the pin is behind. Prose alone loses that; say plainly that the pin is behind. Prose alone loses that;
the test makes stating it the cheaper option. the test makes stating it the cheaper option.
""" """
refs = image_refs() refs = image_refs()
assert refs, "no image references found" assert refs, "no image references found"
pinned = refs[0].split("@", 1)[-1] pinned = refs[0].split("@", 1)[-1]
record = RELEASE_RECORD.read_text(encoding="utf-8") record = json.loads(RELEASE_RECORD.read_text(encoding="utf-8"))
if pinned not in record: assert pinned == record["image"]["digest"], "release evidence must describe the pinned image"
return # a newer artifact is pinned; this record no longer describes it recorded = record["image"]["schema_version"]
assert record["container_verification"]["schema_version"] == recorded
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()
if recorded == LATEST_SCHEMA_VERSION: if recorded == LATEST_SCHEMA_VERSION:
return 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 back-filled to `human`, downgrade unsupported). T03 stays `wait`: gates 1 and 2
are unchanged and nothing is deployed. 125 tests pass. 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 ## Wire outbox delivery and reconciliation
```task ```task