Deploy caller-control receiver and align ledger recovery pins
Some checks failed
CI Smoke / host-smoke (push) Waiting to run
CI Smoke / container-smoke (push) Waiting to run
Transactional mail acceptance / mail (push) Failing after 16s

Assistant: codex
Assistant-Model: gpt-6.1-sol
Assistant-Session: 01a119cf-56d0-7fa3-840b-eb8732e1db0b
This commit is contained in:
tegwick 2026-10-08 14:10:26 +02:00
parent c9756b093f
commit 3e8996fe94
10 changed files with 454 additions and 9 deletions

View file

@ -0,0 +1,253 @@
#!/usr/bin/env python3
"""Exact founder-approved email-connect caller-source release, 2026-10-08.
Default is server dry-run. Never prints SQLite bytes, credentials or child errors.
"""
import argparse
import copy
import hashlib
import json
import os
import signal
import sqlite3
import subprocess
import sys
import time
from pathlib import Path
NS = 'email-connect'
UID = 'a553c742-0115-43d4-99a4-a5ca56fe0786'
OLD = 'forgejo.coulomb.social/coulomb/email-connect@sha256:1e613d43734c44c5cd32c6083fda8b4da5d01ef43d10c6ca9c29a33c788cf75c'
NEW = 'forgejo.coulomb.social/coulomb/email-connect@sha256:eb3e7b68d21209e9eb71b928a5b0bc987e8e3164f21dca4ff712f946d0c103f2'
DECISION = '0ef49fde-5e92-4656-ac1e-8f945b4bba08'
class Refused(RuntimeError):
pass
def k(*args, body=None, timeout=180):
result = subprocess.run(['kubectl', *args], input=body, capture_output=True, timeout=timeout)
if result.returncode:
raise Refused('command_failed')
return result.stdout
def document(*args):
return json.loads(k(*args, '-o', 'json'))
def image(dep):
return dep['spec']['template']['spec']['containers'][0]['image']
def private_write(path, data):
fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
with os.fdopen(fd, 'wb') as stream:
stream.write(data)
stream.flush()
os.fsync(stream.fileno())
def backup_pod(name):
return {'apiVersion': 'v1', 'kind': 'Pod',
'metadata': {'name': name, 'namespace': NS, 'labels': {'email-connect-recovery': name}},
'spec': {'restartPolicy': 'Never', 'automountServiceAccountToken': False,
'securityContext': {'runAsNonRoot': True, 'runAsUser': 10001,
'runAsGroup': 10001, 'fsGroup': 10001, 'seccompProfile': {'type': 'RuntimeDefault'}},
'containers': [{'name': 'backup', 'image': NEW,
'command': ['python', '-c', 'import time;time.sleep(600)'],
'resources': {'requests': {'cpu': '10m', 'memory': '32Mi'},
'limits': {'cpu': '100m', 'memory': '128Mi'}},
'securityContext': {'allowPrivilegeEscalation': False,
'readOnlyRootFilesystem': True, 'capabilities': {'drop': ['ALL']}},
'volumeMounts': [{'name': 'data', 'mountPath': '/data', 'readOnly': True},
{'name': 'tmp', 'mountPath': '/tmp'}]}],
'volumes': [{'name': 'data', 'persistentVolumeClaim':
{'claimName': 'email-connect-data', 'readOnly': True}},
{'name': 'tmp', 'emptyDir': {'sizeLimit':'160Mi'}}]}}
def patch():
return json.dumps([
{'op': 'test', 'path': '/spec/template/spec/containers/0/image', 'value': OLD},
{'op': 'replace', 'path': '/spec/template/spec/containers/0/image', 'value': NEW},
])
def capture(name, destination):
code = '''import os,sqlite3,sys
src=sqlite3.connect('file:/data/email-connect.db?mode=ro',uri=True)
dst=sqlite3.connect('/tmp/verification-pre-release.db')
src.backup(dst)
assert dst.execute('PRAGMA integrity_check').fetchone()[0]=='ok'
dst.close();src.close()
os.chmod('/tmp/verification-pre-release.db',0o600)
with open('/tmp/verification-pre-release.db','rb') as stream:sys.stdout.buffer.write(stream.read())
'''
data = k('exec', '-n', NS, name, '--', 'python', '-c', code)
if not data.startswith(b'SQLite format 3\x00'):
raise Refused('invalid_sqlite_backup')
path = destination / 'database.sqlite3'
private_write(path, data)
with sqlite3.connect('file:' + str(path) + '?mode=ro', uri=True) as database:
if database.execute('PRAGMA integrity_check').fetchone()[0] != 'ok':
raise Refused('invalid_sqlite_backup')
return {'database_sha256': hashlib.sha256(data).hexdigest(),
'database_bytes': len(data), 'sqlite_integrity': 'ok',
'source_pvc_read_only': True, 'writers_quiesced': True}
def rehearse(name):
code = 'import json,sqlite3,hashlib,os\nfrom email_connect.transactional import SQLiteDeliveryStore,ProviderError\nsrc=sqlite3.connect(\'file:/tmp/verification-pre-release.db?mode=ro\',uri=True)\ntarget=\'/tmp/candidate-migration.db\'\ndst=sqlite3.connect(target);src.backup(dst);dst.close()\ntables=[r[0] for r in src.execute("SELECT name FROM sqlite_master WHERE type=\'table\'")]\nassert set(tables)=={\'deliveries\',\'verifications\',\'suppressions\',\'send_attempts\',\'verification_attempts\'}\ncols={t:[r[1] for r in src.execute(\'PRAGMA table_info(\'+t+\')\')] for t in tables}\ndef rows(db,t):return sorted(json.dumps(r) for r in db.execute(\'SELECT \'+\',\'.join(cols[t])+\' FROM \'+t))\nbefore={t:rows(src,t) for t in tables};src.close()\nstore=SQLiteDeliveryStore(target)\nassert all(rows(store.db,t)==before[t] for t in tables)\nassert store.db.execute(\'SELECT COUNT(*) FROM caller_buckets\').fetchone()[0]==0\nassert store.db.execute("SELECT COUNT(*) FROM verifications WHERE caller_id<>\'\'").fetchone()[0]==0\naccepted=unknown=terminal=0\nfor event,fp in store.db.execute(\'SELECT d.event_id,a.fingerprint FROM deliveries d LEFT JOIN send_attempts a USING(event_id)\').fetchall():\n assert store.reserve_send(event,fp or \'synthetic-legacy-probe\') is False;accepted+=1\nfor event,fp,state in store.db.execute("SELECT event_id,fingerprint,state FROM send_attempts WHERE event_id NOT IN (SELECT event_id FROM deliveries)").fetchall():\n if state in (\'in_flight\',\'failed\'):\n try:store.reserve_send(event,fp);raise AssertionError(\'unsafe_retry\')\n except ProviderError as e:assert e.retryable is False\n if state==\'in_flight\':unknown+=1\n else:terminal+=1\nassert all(rows(store.db,t)==before[t] for t in tables)\nassert os.getuid()==10001\nprint(json.dumps({\'uid\':os.getuid(),\'all_five_table_rows_preserved\':True,\'six_table_schema\':True,\'accepted_invitations\':accepted,\'unknown_invitations\':unknown,\'terminal_historical_failures\':terminal,\'mail_sent\':False}))\n'
return json.loads(k('exec', '-n', NS, name, '--', 'python', '-c', code))
def verify():
code = '''import json,os,sqlite3,urllib.request,urllib.error
base='http://127.0.0.1:8080'
results={}
for path in ['/healthz','/readyz']:
with urllib.request.urlopen(base+path,timeout=5) as response:results[path]=response.status
for label,path,payload,token,key,expected,reason in [
('unauthenticated','/v1/send',{},'synthetic-invalid',None,401,'unauthorized'),
('template_denial','/v1/send',{'id':'verification-release-denial','source':'user-engine','type':'arbitrary.send'},os.environ['EMAIL_CONNECT_INGEST_TOKEN'].strip(),'verification-release-denial',400,'template_not_allowed'),
('invalid_verification','/v1/registration-verifications',{},os.environ['EMAIL_CONNECT_INGEST_TOKEN'].strip(),None,400,'invalid_verification_request')]:
headers={'Content-Type':'application/json','Authorization':'Bearer '+token}
if key:headers['Idempotency-Key']=key
request=urllib.request.Request(base+path,data=json.dumps(payload).encode(),headers=headers,method='POST')
try:urllib.request.urlopen(request,timeout=5);raise RuntimeError('expected_denial')
except urllib.error.HTTPError as error:
body=json.load(error)
assert error.code==expected and body.get('error')==reason
results[label]=error.code
with sqlite3.connect('file:/data/email-connect.db?mode=ro',uri=True) as database:
assert database.execute('PRAGMA integrity_check').fetchone()[0]=='ok'
assert database.execute("SELECT 1 FROM sqlite_master WHERE type='table' AND name='caller_buckets'").fetchone()
assert database.execute('SELECT COUNT(*) FROM caller_buckets').fetchone()[0]==0
assert 'caller_id' in {r[1] for r in database.execute('PRAGMA table_info(verifications)')}
assert {'error','retryable'} <= {r[1] for r in database.execute('PRAGMA table_info(send_attempts)')}
assert 'EMAIL_CONNECT_CALLERS_FILE' not in os.environ
results.update(sqlite_integrity='ok',caller_schema_present=True,scoped_policy_active=False,mail_sent=False)
print(json.dumps(results))
'''
return json.loads(k('exec', '-n', NS, 'deployment/email-connect', '--', 'python', '-c', code))
def deploy(before, body, destination):
destination.mkdir(mode=0o700)
receipt = {'status': 'incomplete', 'decision': DECISION, 'old_image': OLD,
'new_image': NEW, 'started_at': time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())}
started = time.monotonic()
paused = False
helper = False
succeeded = False
name = json.loads(body)['metadata']['name']
try:
paused = True
receipt['phase'] = 'pause'
k('patch', 'deployment/email-connect', '-n', NS, '--type=json', '-p', json.dumps([
{'op':'test','path':'/spec/template','value':before['spec']['template']},
{'op':'test','path':'/spec/replicas','value':1},
{'op':'replace','path':'/spec/replicas','value':0}]))
k('wait', '--for=delete', 'pod', '-n', NS,
'-l', 'app.kubernetes.io/name=email-connect', '--timeout=120s')
if document('get', 'deployment', 'email-connect', '-n', NS)['spec']['replicas'] != 0:
raise Refused('writers_not_quiesced')
helper = True
k('create', '-f', '-', body=body)
k('wait', '--for=condition=Ready', 'pod/' + name, '-n', NS, '--timeout=180s', timeout=240)
receipt['phase'] = 'capture'
receipt.update(capture(name, destination))
receipt['phase'] = 'isolated_migration'
receipt['migration_checks'] = rehearse(name)
k('delete', 'pod', name, '-n', NS, '--wait=true', '--timeout=120s')
helper = False
receipt['phase'] = 'image_patch'
k('patch', 'deployment', 'email-connect', '-n', NS, '--type=json', '-p', patch())
k('scale', 'deployment/email-connect', '-n', NS, '--replicas=1')
k('rollout', 'status', 'deployment/email-connect', '-n', NS, '--timeout=180s', timeout=240)
actual = document('get', 'deployment', 'email-connect', '-n', NS)
expected = copy.deepcopy(before['spec'])
expected['template']['spec']['containers'][0]['image'] = NEW
if actual['spec'] != expected or actual['status'].get('readyReplicas') != 1:
raise Refused('unexpected_release_spec')
receipt['phase'] = 'verification'
receipt['checks'] = verify()
receipt.update(status='released_verified', image_only_spec_change=True, ready_replicas=1)
succeeded = True
except BaseException as exc:
receipt.update(status='failed', failure_type=type(exc).__name__)
raise
finally:
try:
if helper:
k('delete', 'pod', name, '-n', NS, '--ignore-not-found=true', '--wait=true', '--timeout=120s')
except Exception:
receipt['helper_cleanup_failed'] = True
if paused and not succeeded:
try:
current = image(document('get', 'deployment', 'email-connect', '-n', NS))
replicas = '1' if current == OLD else '0'
k('scale', 'deployment/email-connect', '-n', NS, '--replicas=' + replicas)
receipt['failure_replicas'] = int(replicas)
if replicas == '1':
k('rollout', 'status', 'deployment/email-connect', '-n', NS, '--timeout=180s', timeout=240)
receipt['old_receiver_resumed'] = True
except Exception:
receipt['receiver_state_requires_check'] = True
receipt['elapsed_seconds'] = round(time.monotonic() - started, 2)
private_write(destination / 'release-receipt.json', json.dumps(receipt, indent=2).encode())
print(json.dumps(receipt))
return receipt
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--execute', action='store_true')
parser.add_argument('--destination', type=Path, required=True)
args = parser.parse_args()
os.environ['KUBECONFIG'] = '/etc/rancher/k3s/k3s.yaml'
if document('get', 'namespace', 'kube-system')['metadata']['uid'] != UID:
raise Refused('wrong_cluster')
before = document('get', 'deployment', 'email-connect', '-n', NS)
if image(before) != OLD or before['spec']['replicas'] != 1 or before['status'].get('readyReplicas') != 1:
raise Refused('unexpected_release')
if document('get', 'pvc', 'email-connect-data', '-n', NS)['status']['phase'] != 'Bound':
raise Refused('pvc_not_bound')
config = document('get', 'configmap', 'email-connect-config', '-n', NS)['data']
if 'EMAIL_CONNECT_CALLERS_FILE' in config:
raise Refused('scoped_policy_not_in_release')
flags = json.loads(k('exec', '-n', NS, 'deployment/email-connect', '--', 'python', '-c',
"import os,json;print(json.dumps({'scoped': 'EMAIL_CONNECT_CALLERS_FILE' in os.environ}))"))
if flags['scoped']:
raise Refused('unexpected_live_policy')
if config.get('EMAIL_CONNECT_DATABASE_PATH') != '/data/email-connect.db':
raise Refused('unexpected_database_path')
name = 'email-connect-callers-backup-' + time.strftime('%Y%m%d%H%M%S', time.gmtime())
body = json.dumps(backup_pod(name)).encode()
k('create', '--dry-run=server', '-f', '-', body=body)
projected = document('patch', 'deployment', 'email-connect', '-n', NS,
'--type=json', '-p', patch(), '--dry-run=server')
expected = copy.deepcopy(before['spec'])
expected['template']['spec']['containers'][0]['image'] = NEW
if projected['spec'] != expected:
raise Refused('unexpected_dry_run_spec')
if not args.execute:
print(json.dumps({'status': 'reviewed', 'live_changes': False,
'image_only_spec_diff': True, 'readonly_backup_helper': True}))
return
if not args.destination.is_absolute() or args.destination.exists():
raise Refused('destination_must_be_new')
def terminate(signum, frame):
raise Refused('release_interrupted')
signal.signal(signal.SIGTERM, terminate)
deploy(before, body, args.destination)
if __name__ == '__main__':
try:
main()
except Exception as exc:
print(json.dumps({'status': 'failed', 'failure_type': type(exc).__name__}))
sys.exit(1)

View file

@ -42,8 +42,8 @@ spec:
terminationGracePeriodSeconds: 15
containers:
- name: email-connect
# Verification retry/restart correction, source 7258505; candidate pending founder go.
image: forgejo.coulomb.social/coulomb/email-connect@sha256:1e613d43734c44c5cd32c6083fda8b4da5d01ef43d10c6ca9c29a33c788cf75c
# Caller controls and terminal invitation retry fix, source c9756b0; approved image-only rollout.
image: forgejo.coulomb.social/coulomb/email-connect@sha256:eb3e7b68d21209e9eb71b928a5b0bc987e8e3164f21dca4ff712f946d0c103f2
imagePullPolicy: IfNotPresent
ports:
- name: http

View file

@ -1,8 +1,9 @@
# Scoped transactional callers — source review, 2026-10-08
The receiver can isolate explicitly admitted callers while retaining the existing
single-caller startup path until deployment admission. No live credentials,
mailboxes, sender, chart, image, ledger or timer changed in this source increment.
single-caller startup path until deployment admission. The initial source increment changed no runtime. The tested image is now
deployed in the existing single-caller mode; see caller-deployment-2026-10-08.md.
Scoped policy admission and new caller activation remain separate.
Set EMAIL_CONNECT_CALLERS_FILE to an owner-controlled read-only JSON policy file.
If present, invalid policy fails startup before ledger migration; there is no
@ -53,9 +54,9 @@ denies its operations without deleting transport/idempotency evidence.
Startup adds caller_id to verification rows, error/retryable to send_attempts,
and caller_buckets. Capture fingerprints include all six tables; old five-table
captures remain readable. Rehearsal source preserves scoped accepted/uncertain
verification reservations and budgets without provider calls. Helpers remain
pinned to the current admitted image: repin capture and rehearsal together to
the candidate's immutable image before new-schema capture or scoped activation.
verification reservations and budgets without provider calls. Capture/rehearsal helper source pins are repinned together to the deployed
eb3e7b68 immutable image. Their current guarded capture still requires explicit
archive admission; scoped policy activation remains a separate operation.
Do not downgrade after new-schema rows exist without an admitted compatible
recovery plan; old receiver code can lose retry restrictions or fail inserts.

View file

@ -0,0 +1,48 @@
{
"status": "released_verified",
"decision": "0ef49fde-5e92-4656-ac1e-8f945b4bba08",
"old_image": "forgejo.coulomb.social/coulomb/email-connect@sha256:1e613d43734c44c5cd32c6083fda8b4da5d01ef43d10c6ca9c29a33c788cf75c",
"new_image": "forgejo.coulomb.social/coulomb/email-connect@sha256:eb3e7b68d21209e9eb71b928a5b0bc987e8e3164f21dca4ff712f946d0c103f2",
"started_at": "2026-10-08T12:05:17Z",
"phase": "verification",
"database_sha256": "1c943f28517cb75f324f1b11ed2e24f9aaa9779992a8534ef85906c1ff09dcae",
"database_bytes": 49152,
"sqlite_integrity": "ok",
"source_pvc_read_only": true,
"writers_quiesced": true,
"migration_checks": {
"uid": 10001,
"all_five_table_rows_preserved": true,
"six_table_schema": true,
"accepted_invitations": 2,
"unknown_invitations": 0,
"terminal_historical_failures": 0,
"mail_sent": false
},
"checks": {
"/healthz": 200,
"/readyz": 200,
"unauthenticated": 401,
"template_denial": 400,
"invalid_verification": 400,
"sqlite_integrity": "ok",
"caller_schema_present": true,
"scoped_policy_active": false,
"mail_sent": false
},
"image_only_spec_change": true,
"ready_replicas": 1,
"elapsed_seconds": 62.79,
"source_commit": "c9756b0",
"local_packaged_rehearsal": {
"uid": 10001,
"all_five_table_rows_preserved": true,
"six_table_schema": true,
"accepted_invitations": 2,
"unknown_invitations": 0,
"terminal_historical_failures": 0,
"mail_sent": false
},
"synthetic_release_failure_tests": 7,
"backup_custody": "private temporary copies; durable custody admission pending"
}

View file

@ -0,0 +1,56 @@
# Email receiver deployment — 2026-10-08
Released under the user's explicit “If not please do so” instruction and
State Hub decision 0ef49fde-5e92-4656-ac1e-8f945b4bba08. Source c9756b0 (runtime code f703bf6),
image forgejo.coulomb.social/coulomb/email-connect@sha256:eb3e7b68d21209e9eb71b928a5b0bc987e8e3164f21dca4ff712f946d0c103f2. Deployment revision7, one Ready replica.
Started 2026-10-08T12:05:17Z; elapsed 62.79s.
The live Deployment spec changed only its immutable image. Existing caller token,
sender, config, PVC, ServiceAccount and networking are retained. Scoped policy
is not selected: per-caller admission is a separate operation in EMAIL-WP-0008-T03
and CSOC-WP-0015-T01. Permanent invitation rejection now remains terminal in the
existing startup mode; new caller controls are available for admitted policy.
No new caller, credential, sender, mailbox access, offsite upload or timer is
activated. No mail was sent for release verification.
Preflight: pinned cluster UID a553c742-0115-43d4-99a4-a5ca56fe0786,
old image1e613d43 and one Ready replica, Bound existing PVC, expected database
path and no selected caller policy. Node Ready/no pressure, CPU requests3455m
of4000m, memory9440Mi, disk120Gi free. Server dry-run proves image-only spec.
Seven offline failure scenarios prove pre-patch old-receiver resumption and
post-patch stopped-receiver fencing. Guard tests full template and replica count
before quiescing; historical helper cannot repeat this completed rollout.
Pause writer, take SQLite backup through read-only data-PVC helper without runtime
credentials, rehearse new-schema migration in its private copy, remove helper,
patch old-image test→new image, resume and verify. Every row and old column in
all five pre-release tables remains unchanged; caller namespace defaults empty,
caller budget table empty, two accepted invitation reservations remain duplicate
receipts. Isolated local packaged UID10001/read-only-root/network-none migration
of the actual copied backup passes the same proof. Synthetic packaged tests prove
scoped restart/idempotency, scope denial and redacted reference behavior.
Postflight: healthz/readyz200, unauthorized401, admitted bearer/disallowed
template400, invalid verification400, SQLite integrityok, six-table caller schema,
zero budget records and no selected scoped policy. One Ready replica, only image
spec change and no capture helper left. Public receipt:
caller-deployment-2026-10-08.json. Guarded capture and rehearsal helper source pins
are repinned together to the new digest; focused backup/release gates pass.
Private pre-release SQLite:49152bytes,
SHA256 1c943f28517cb75f324f1b11ed2e24f9aaa9779992a8534ef85906c1ff09dcae.
Node root0700 /tmp/email-connect-precallers-20261008/database.sqlite3 and
workstation0700 /tmp/csoc-mail-private-j4qnemvw/database.sqlite3 hold matching mode0600 copies.
These are temporary custody, not governed offsite recovery. Their retention/
expiry stays live in CSOC-WP-0015-T03 and EMAIL-WP-0007-T03/T04.
Rollback: stop the receiver first and preserve current ledger plus private backup.
Prefer rolling forward. Old code ignores caller ownership/budgets and terminal
failure policy, and old inserts can fail against added columns. Never use
unrestricted rollout undo or overwrite the current ledger with a pre-release
snapshot after traffic: reconcile later provider acceptance and challenge use
before any release of restored traffic. Scope/mode change needs separate return.
Remaining product work is CSOC-WP-0015 (runtime policy, sender/reply/bounce,
independent ledger custody and isolated owner-browser/flow acceptance), with
EMAIL-WP-0008-T03 and EMAIL-WP-0007-T03/T04 as canonical owner tasks.

View file

@ -20,7 +20,7 @@ NS = "email-connect"
CLUSTER_UID = "a553c742-0115-43d4-99a4-a5ca56fe0786"
IMAGE = (
"forgejo.coulomb.social/coulomb/email-connect@sha256:"
"1e613d43734c44c5cd32c6083fda8b4da5d01ef43d10c6ca9c29a33c788cf75c"
"eb3e7b68d21209e9eb71b928a5b0bc987e8e3164f21dca4ff712f946d0c103f2"
)
LOCK_PATH = Path("/var/lib/email-connect-backup/capture.lock")
SELECTOR = "app.kubernetes.io/name=email-connect"

View file

@ -20,7 +20,7 @@ from ledger_archive import (
IMAGE = (
"forgejo.coulomb.social/coulomb/email-connect@sha256:"
"1e613d43734c44c5cd32c6083fda8b4da5d01ef43d10c6ca9c29a33c788cf75c"
"eb3e7b68d21209e9eb71b928a5b0bc987e8e3164f21dca4ff712f946d0c103f2"
)
CODE = """import io,json,os,sys

View file

@ -0,0 +1,57 @@
"""Exact release failure fences, mocked Kubernetes; no mail or cluster calls."""
import copy
import importlib.util
import json
from pathlib import Path
import pytest
@pytest.mark.parametrize('failure', ['capture','migration','patch_before','patch_unknown','rollout','verify',None])
def test_release_preserves_old_receiver_or_holds_new_receiver_stopped(tmp_path, monkeypatch, failure):
script=Path(__file__).resolve().parents[1]/'deploy/k8s/railiance/deploy-callers-2026-10-08.py'
spec=importlib.util.spec_from_file_location('caller_release',script)
module=importlib.util.module_from_spec(spec);spec.loader.exec_module(module)
before={'spec':{'replicas':1,'template':{'spec':{'containers':[{'image':module.OLD}]} }},'status':{'readyReplicas':1}}
state=copy.deepcopy(before)
def document(*args):return copy.deepcopy(state)
def k(*args, **kwargs):
if args[0]=='patch':
operations=json.loads(args[args.index('-p')+1])
if operations[0]['path']=='/spec/template':
assert state['spec']['template']==operations[0]['value']
assert state['spec']['replicas']==1
state['spec']['replicas']=0
else:
assert module.image(state)==module.OLD
if failure=='patch_before':raise module.Refused('synthetic_failure')
state['spec']['template']['spec']['containers'][0]['image']=module.NEW
if failure=='patch_unknown':raise module.Refused('synthetic_lost_response')
if args[0]=='scale':state['spec']['replicas']=int(args[-1].split('=')[1])
if args[0]=='rollout' and module.image(state)==module.NEW and failure=='rollout':
raise module.Refused('synthetic_failure')
return b''
def capture(*args):
if failure=='capture':raise module.Refused('synthetic_failure')
return {'database_sha256':'synthetic','database_bytes':1}
def rehearse(*args):
if failure=='migration':raise module.Refused('synthetic_failure')
return {'all_five_table_rows_preserved':True,'mail_sent':False}
def verify():
if failure=='verify':raise module.Refused('synthetic_failure')
return {'mail_sent':False,'scoped_policy_active':False}
for name,value in [('k',k),('document',document),('capture',capture),('rehearse',rehearse),('verify',verify)]:
monkeypatch.setattr(module,name,value)
destination=tmp_path/'release'
body=json.dumps({'metadata':{'name':'synthetic-helper'}}).encode()
if failure:
with pytest.raises(module.Refused):module.deploy(before,body,destination)
receipt=json.loads((destination/'release-receipt.json').read_text())
assert receipt['status']=='failed'
changed=failure in {'patch_unknown','rollout','verify'}
assert module.image(state)==(module.NEW if changed else module.OLD)
assert state['spec']['replicas']==(0 if changed else 1)
else:
receipt=module.deploy(before,body,destination)
assert receipt['status']=='released_verified'
assert state['spec']['replicas']==1 and module.image(state)==module.NEW
assert destination.stat().st_mode & 0o777==0o700
assert (destination/'release-receipt.json').stat().st_mode & 0o777==0o600

View file

@ -112,3 +112,14 @@ activation. Complete review: docs/ledger-backup-review-2026-10-08.md. Platform
admission handoff RPF-WP-0053-T05 is a live record, not an owner approval. T03/T04
remain wait for custody, retention/capacity, actual offsite/recovery and monitoring
plus the incident post-capture reconciliation gate. The workplan remains active.
## Receiver image/schema alignment — 2026-10-08
EMAIL-WP-0008-T04 deploys receiver eb3e7b68 (sourcec9756b0) in the admitted
single-caller mode. The ledger has caller_id/send-failure fields and caller_buckets.
Capture/rehearsal source helpers are repinned together to this digest; fingerprints
cover all six tables. Actual pre-release five-table backup migrates privately
with every existing row preserved, UID10001/network-none and no send. See
docs/caller-deployment-2026-10-08.md. This is image/schema alignment only: T03/T04
archive custody, first admitted offsite run/retrieval/application recovery and
monitoring stay waiting. CSOC-WP-0015-T03 is the product acceptance follow-up.

View file

@ -72,3 +72,22 @@ RPF-WP-0053-T05/T06 retain offsite archive/atomic credential pair custody.
CSOC-WP-0010-T04/T05/T06 retain platform operations and productive/browser review.
Review: docs/caller-admission-2026-10-08.md. No sender, caller, token, image,
mailbox, production ledger, network admission, upload or timer is activated.
## T04 — Deploy the tested receiver source in the admitted startup mode
```task
id: EMAIL-WP-0008-T04
status: done
priority: high
```
User explicitly instructed deployment. Image eb3e7b68 from sourcec9756b0 is
live, revision7, one Ready replica. Image-only spec guard/server dry-run,
quiesced private backup, five-table row preservation/new-schema migration,
packaged UID10001/no-network rehearsal and no-send denial checks pass.
Elapsed62.79s; decision0ef49fde-5e92-4656-ac1e-8f945b4bba08. No scoped policy, sender/caller
change or timer activated. Capture/rehearsal helper source pins move together to
this digest; old-version recovery and downgrade require their bound receipts.
Review: docs/caller-deployment-2026-10-08.md and JSON receipt.
T03 stays wait for actual scoped-policy/credential admission. Product follow-up
CSOC-WP-0015 and existing EMAIL-WP-0007/RPF-WP-0053 retain operations/recovery.