Implement signed release admission and durable rollback coordinator
All checks were successful
CI Smoke / host-smoke (push) Successful in 1s
CI Smoke / container-smoke (push) Successful in 3s
Build and Publish Container Image / build-and-push (push) Successful in 1m32s

Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a0e241-8285-7a63-8c0c-51c9cb824dc3
This commit is contained in:
tegwick 2026-09-27 16:39:29 +02:00
parent a122b1a5a4
commit 22935955cc
10 changed files with 810 additions and 59 deletions

View file

@ -14,7 +14,7 @@ COPY .forgejo/workflows/image.yaml ./.forgejo/workflows/image.yaml
COPY schemas/ ./schemas/ COPY schemas/ ./schemas/
COPY k8s/ ./k8s/ COPY k8s/ ./k8s/
COPY scripts/render_gitops.py scripts/check_gitops_promotion.py ./scripts/ COPY scripts/render_gitops.py scripts/check_gitops_promotion.py ./scripts/
RUN uv sync --frozen --extra dev && uv run --no-sync python scripts/render_gitops.py --check --verify-frontend --verify-platform && uv run --no-sync pytest -q -p no:cacheprovider tests/test_frontend_patterns.py tests/test_instruction_evaluation.py tests/test_admin_sync_api.py tests/test_gitops_release.py RUN uv sync --frozen --extra dev && uv run --no-sync python scripts/render_gitops.py --check --verify-frontend --verify-platform && uv run --no-sync pytest -q -p no:cacheprovider tests/test_frontend_patterns.py tests/test_instruction_evaluation.py tests/test_admin_sync_api.py tests/test_gitops_release.py tests/test_release_broker.py
# Stage 2 — runtime image # Stage 2 — runtime image
FROM python:3.12-slim@sha256:78387bc3881b8273120a12ebe6c1ab22b018ccc2c9adf565ae1ac9b536e184ea AS runtime FROM python:3.12-slim@sha256:78387bc3881b8273120a12ebe6c1ab22b018ccc2c9adf565ae1ac9b536e184ea AS runtime

72
docs/release-broker.md Normal file
View file

@ -0,0 +1,72 @@
# Image-only release broker core
ACTIVITY-WP-0041-T03 owns this implementation. **Not activated in production.**
The new modules are not wired to an API, worker, schedule or credential source.
Construction requires `admitted=True` from trusted deployment configuration; that
switch is a local guard, not proof that an identity has actually been admitted.
The current deployed revision is unchanged, so this code does not restart soak.
## Implemented boundary
`activity_core.release_broker.Receipts` checks Ed25519 signatures against a locally
configured key-to-principal/role registry. Requests cannot introduce trusted keys.
Build, independent review, health and retention attestations must all name the
same fixed repository/application, full candidate and rollback commits, and exact
before/after manifest hashes. Evidence expires within five minutes. Build and
review require different principals **and keys**. Build evidence must cover exact
candidate image digests and all three mandatory CI contexts. Retention must cover
both live and rollback images. Health must attest continuous healthy observation
of the prior revision for at least 24 hours. The trusted issuers must verify these
facts against their authorities; signatures alone cannot make assertions true.
`gitops_policy` is the shared image-only validator used by both broker and the
existing CLI. It forbids non-image deployment changes and resource-set changes.
`release_operations` constructs a one-field update of the validated platform child
Application and fixed selective root/child sync payloads. No arbitrary command,
repository path, application, prune flag or override is exposed by these builders.
## Durable recovery
A local SQLite ledger stores signed receipts, their hashes, the release binding,
phase and transition history. One active row and an immediate transaction serialize
all releases and adapter calls on that ledger. The database must reside on durable,
operator-owned local storage, shared by all instances handling this application;
this is not a distributed lock across independent databases or network filesystems.
Phases are planned → publish_pending → published → synced → complete. Publication
intent is committed before calling the adapter. Lost responses therefore cannot
turn a possibly published change into an assumed cancellation. A release that
expires before any attempt is cancelled; after intent, timeout or failed health
enters rollback_planned → rollback_published → rollback_synced → rolled_back.
Failed rollback retains the active slot. Retried publication/sync must be
idempotent, using compare-and-swap and accepting already-at-target as success.
The adapter must refuse any unexpected third revision rather than overwriting it.
Tests use generated fixture keys and an in-memory adapter. They prove policy and
state-machine behavior, including restart and failed-health rollback; they are
**not** proof of production Git/ArgoCD rollback or admitted authority.
## Still required before activation
1. Implement and verify an authenticated transport adapter that resolves exact
source commits, compares rendered manifests to the signed hashes, publishes
only the platform child revision field, and runs the fixed selective syncs.
Every network operation needs a bounded timeout; health must wait within that
bound for the exact revision and verify deployments, report sink and schedules.
It must persist/recover the platform commit across lost responses and serialize
with other writers. A generic repository-write token is not path enforcement.
2. Admit the dedicated principal and credential custody through the owner lane.
ArgoCD Core has no API-server token lane. Kubernetes Application patch RBAC
alone cannot restrict fields: keep it behind the reviewed broker boundary.
Publish negative access tests and revocation behavior; no broad operator key.
3. Supply independent trusted build/review/health/retention issuers and their key
custody/rotation. The observer must measure continuous health; it must not
manufacture a 24-hour interval from two snapshots. Preserve signing public
keys for audit and protect the ledger from producer writes.
4. Connect the durable dispatcher through activity-core/Temporal and sanitized
evidence sinks. Prove authenticated transport failure, concurrency with other
publishers, restart, denial and rollback in an isolated deployment environment.
Then finish the production observation gate and enable the bounded scope.
Platform enforcement contract: `railiance-platform/docs/activity-core-release-admission.md`.
These requirements remain live work in ACTIVITY-WP-0041-T03 and RPF-WP-0048-T02.

View file

@ -13,6 +13,7 @@ dependencies = [
"nats-py>=2.7", "nats-py>=2.7",
"httpx>=0.27", "httpx>=0.27",
"pyyaml>=6.0", "pyyaml>=6.0",
"cryptography>=44.0",
] ]
[project.scripts] [project.scripts]

View file

@ -1,63 +1,19 @@
"""Fail-closed admission check for a candidate image-only GitOps release. """Compatibility CLI for the shared image-only release admission policy."""
This is a validator, not an authority issuer or a cluster/Git credential broker.
Evidence must be supplied by the separately admitted release identity.
"""
import argparse import argparse
import copy
from datetime import datetime, timedelta, timezone
import json import json
from pathlib import Path from pathlib import Path
import re
import yaml import yaml
IMAGE=re.compile(r'forgejo\.coulomb\.social/coulomb/activity-core@sha256:[0-9a-f]{64}') from activity_core.gitops_policy import validate
SHA=re.compile(r'[0-9a-f]{40}')
DEPLOYS={'actcore-api','actcore-worker','actcore-event-router'}
def require(condition,message):
if not condition: raise ValueError(message)
def indexed(docs):
out={}
for d in docs:
require(isinstance(d,dict),'invalid resource')
key=(d['kind'],d['metadata']['name'])
require(key not in out and d['metadata'].get('namespace')=='activity-core','duplicate or foreign resource')
out[key]=copy.deepcopy(d)
return out
def validate(before,after,evidence,now=None): if __name__ == "__main__":
now=now or datetime.now(timezone.utc) parser = argparse.ArgumentParser()
require(evidence.get('schema_version')==1,'unknown evidence schema') parser.add_argument("before", type=Path)
require(evidence.get('identity_admitted') is True,'release identity not admitted') parser.add_argument("after", type=Path)
require(evidence.get('authority')=='ACTIVITY-WP-0041-image-only-v1','unknown authority') parser.add_argument("evidence", type=Path)
require(evidence.get('review_result')=='pass' and evidence.get('checks')=='pass','checks/review not passed') args = parser.parse_args()
require(evidence.get('reviewer') and evidence.get('reviewer')!=evidence.get('producer'),'independent review required') try:
require(bool(SHA.fullmatch(evidence.get('candidate_commit',''))),'full candidate revision required') print(json.dumps(validate(list(yaml.safe_load_all(args.before.read_text())),
require(bool(SHA.fullmatch(evidence.get('rollback_commit',''))),'rollback revision required') list(yaml.safe_load_all(args.after.read_text())),
require(evidence['candidate_commit']!=evidence['rollback_commit'],'rollback must name prior revision') json.loads(args.evidence.read_text()))))
observed=datetime.fromisoformat(evidence['healthy_since'].replace('Z','+00:00')) except (ValueError, KeyError, TypeError) as exc:
measured=datetime.fromisoformat(evidence['observed_at'].replace('Z','+00:00')) raise SystemExit(f"refused: {exc}")
require(observed.tzinfo is not None and measured.tzinfo is not None,'timestamps need timezone')
require(now-timedelta(minutes=5)<=measured<=now,'health evidence stale or future')
require(measured-observed>=timedelta(hours=24),'24-hour healthy observation period incomplete')
require(evidence.get('argo_synced') is True and evidence.get('argo_healthy') is True,'Argo not healthy/synced')
left,right=indexed(before),indexed(after)
require(left.keys()==right.keys(),'resource inventory change')
changed=[]
for key,a in left.items():
b=right[key]
if a==b: continue
require(key[0]=='Deployment' and key[1] in DEPLOYS,'only runtime images may change')
ac=a['spec']['template']['spec']['containers'];bc=b['spec']['template']['spec']['containers']
require(len(ac)==len(bc)==1,'container inventory change')
require(bool(IMAGE.fullmatch(bc[0].get('image',''))),'registry digest required')
require(bc[0].get('imagePullPolicy')=='IfNotPresent','pull policy must support registry recovery')
ac[0]['image']=bc[0]['image'];ac[0]['imagePullPolicy']=bc[0]['imagePullPolicy']
require(a==b,'non-image deployment change')
changed.append(key[1])
require(bool(changed),'no release change')
return {'admitted':True,'deployments':changed,'candidate_commit':evidence['candidate_commit'],'rollback_commit':evidence['rollback_commit']}
if __name__=='__main__':
p=argparse.ArgumentParser();p.add_argument('before',type=Path);p.add_argument('after',type=Path);p.add_argument('evidence',type=Path);a=p.parse_args()
try: print(json.dumps(validate(list(yaml.safe_load_all(a.before.read_text())),list(yaml.safe_load_all(a.after.read_text())),json.loads(a.evidence.read_text()))))
except (ValueError,KeyError,TypeError) as e: raise SystemExit(f'refused: {e}')

View file

@ -0,0 +1,55 @@
"""Fail-closed admission check for a candidate image-only GitOps release.
This is a validator, not an authority issuer or a cluster/Git credential broker.
Evidence must be supplied by the separately admitted release identity.
"""
import copy
from datetime import datetime, timedelta, timezone
import re
import yaml
IMAGE=re.compile(r'forgejo\.coulomb\.social/coulomb/activity-core@sha256:[0-9a-f]{64}')
SHA=re.compile(r'[0-9a-f]{40}')
DEPLOYS={'actcore-api','actcore-worker','actcore-event-router'}
def require(condition,message):
if not condition: raise ValueError(message)
def indexed(docs):
out={}
for d in docs:
require(isinstance(d,dict),'invalid resource')
key=(d['kind'],d['metadata']['name'])
require(key not in out and d['metadata'].get('namespace')=='activity-core','duplicate or foreign resource')
out[key]=copy.deepcopy(d)
return out
def validate(before,after,evidence,now=None):
now=now or datetime.now(timezone.utc)
require(evidence.get('schema_version')==1,'unknown evidence schema')
require(evidence.get('identity_admitted') is True,'release identity not admitted')
require(evidence.get('authority')=='ACTIVITY-WP-0041-image-only-v1','unknown authority')
require(evidence.get('review_result')=='pass' and evidence.get('checks')=='pass','checks/review not passed')
require(evidence.get('reviewer') and evidence.get('reviewer')!=evidence.get('producer'),'independent review required')
require(bool(SHA.fullmatch(evidence.get('candidate_commit',''))),'full candidate revision required')
require(bool(SHA.fullmatch(evidence.get('rollback_commit',''))),'rollback revision required')
require(evidence['candidate_commit']!=evidence['rollback_commit'],'rollback must name prior revision')
observed=datetime.fromisoformat(evidence['healthy_since'].replace('Z','+00:00'))
measured=datetime.fromisoformat(evidence['observed_at'].replace('Z','+00:00'))
require(observed.tzinfo is not None and measured.tzinfo is not None,'timestamps need timezone')
require(now-timedelta(minutes=5)<=measured<=now,'health evidence stale or future')
require(measured-observed>=timedelta(hours=24),'24-hour healthy observation period incomplete')
require(evidence.get('argo_synced') is True and evidence.get('argo_healthy') is True,'Argo not healthy/synced')
left,right=indexed(before),indexed(after)
require(left.keys()==right.keys(),'resource inventory change')
changed=[]
for key,a in left.items():
b=right[key]
if a==b: continue
require(key[0]=='Deployment' and key[1] in DEPLOYS,'only runtime images may change')
ac=a['spec']['template']['spec']['containers'];bc=b['spec']['template']['spec']['containers']
require(len(ac)==len(bc)==1,'container inventory change')
require(bool(IMAGE.fullmatch(bc[0].get('image',''))),'registry digest required')
require(bc[0].get('imagePullPolicy')=='IfNotPresent','pull policy must support registry recovery')
ac[0]['image']=bc[0]['image'];ac[0]['imagePullPolicy']=bc[0]['imagePullPolicy']
require(a==b,'non-image deployment change')
changed.append(key[1])
require(bool(changed),'no release change')
return {'admitted':True,'deployments':changed,'candidate_commit':evidence['candidate_commit'],'rollback_commit':evidence['rollback_commit']}

View file

@ -0,0 +1,218 @@
"""Durable image-release coordinator; production adapters are deliberately separate.
Trust keys and role bindings come from operator-owned configuration, never requests.
Adapters must implement the fixed Git/ArgoCD contract documented in release admission.
No credential loading, generic command execution, HTTP endpoint or live adapter here.
"""
from __future__ import annotations
import base64
import hashlib
import json
import sqlite3
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Protocol
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
from .gitops_policy import validate
ROLES = {"build", "review", "health", "retention"}
AUTHORITY = "ACTIVITY-WP-0041-image-only-v1"
TERMINAL = {"complete", "rolled_back", "cancelled"}
REQUIRED_CHECKS = {"CI Smoke / host-smoke (push)", "CI Smoke / container-smoke (push)",
"Build and Publish Container Image / build-and-push (push)"}
def canonical(value):
return json.dumps(value, sort_keys=True, separators=(",", ":"), allow_nan=False).encode()
def fingerprint(value):
return hashlib.sha256(canonical(value)).hexdigest()
def timestamp(value):
result = datetime.fromisoformat(value.replace("Z", "+00:00"))
if result.tzinfo is None:
raise ValueError("timezone required")
return result
class Receipts:
"""Verify Ed25519 envelopes against locally bound principals and roles."""
def __init__(self, trusted):
# key id -> {public_key: raw 32 bytes, principal: str, roles: set[str]}
self.trusted = trusted
def verify(self, envelope, role, binding, now):
if set(envelope) != {"key_id", "payload", "signature"}:
raise ValueError("unexpected envelope fields")
trust = self.trusted.get(envelope["key_id"])
if trust is None or role not in trust["roles"]:
raise ValueError("untrusted receipt role")
payload = envelope["payload"]
signature = base64.b64decode(envelope["signature"], validate=True)
Ed25519PublicKey.from_public_bytes(trust["public_key"]).verify(signature, canonical(payload))
if (payload.get("role") != role or payload.get("schema") != 1
or payload.get("authority") != AUTHORITY or payload.get("binding") != binding
or payload.get("result") != "pass"):
raise ValueError("receipt scope/result mismatch")
issued, expires = timestamp(payload["issued_at"]), timestamp(payload["expires_at"])
if not now - timedelta(minutes=5) <= issued <= now < expires <= issued + timedelta(minutes=5):
raise ValueError("stale or future receipt")
return trust["principal"], payload
class Backend(Protocol):
"""Trusted adapter, never implemented from caller-provided URLs or commands.
publish_revision is a CAS on the single platform child targetRevision field;
already-at-target is success, any third revision is a conflict. It must retain
source signatures/checks, serialize with other writers and return only after
durable Git publication. sync_revision selects ONLY the activity-core child in
the root, then ONLY the child application, with prune false and no overrides.
Both methods must be idempotent after a lost response or broker restart.
healthy includes exact revision, deployment health, report/schedule invariants.
"""
def publish_revision(self, expected: str, target: str) -> None: ...
def sync_revision(self, target: str) -> None: ...
def healthy(self, target: str) -> bool: ...
class Broker:
def __init__(self, database: Path, receipts: Receipts, *, admitted=False):
self.database, self.receipts, self.admitted = database, receipts, admitted
with self.connect() as db:
db.execute("CREATE TABLE IF NOT EXISTS releases (id TEXT PRIMARY KEY, plan TEXT NOT NULL, phase TEXT NOT NULL, active INTEGER UNIQUE, deadline TEXT NOT NULL, error TEXT)")
db.execute("CREATE TABLE IF NOT EXISTS transitions (sequence INTEGER PRIMARY KEY, release_id TEXT NOT NULL, phase TEXT NOT NULL, observed_at TEXT NOT NULL)")
def connect(self):
db = sqlite3.connect(self.database, timeout=5, isolation_level=None)
db.execute("PRAGMA synchronous=FULL")
return db
def admit(self, before, after, candidate, rollback, envelopes, now=None):
if not self.admitted:
raise ValueError("broker identity not admitted")
now = now or datetime.now(timezone.utc)
binding = {"repository": "coulomb/activity-core", "application": "activity-core",
"candidate": candidate, "rollback": rollback,
"before_sha256": fingerprint(before), "after_sha256": fingerprint(after)}
if set(envelopes) != ROLES:
raise ValueError("all independent receipt roles required")
verified = {r: self.receipts.verify(envelopes[r], r, binding, now) for r in ROLES}
if (verified["build"][0] == verified["review"][0]
or self.receipts.trusted[envelopes["build"]["key_id"]]["public_key"]
== self.receipts.trusted[envelopes["review"]["key_id"]]["public_key"]):
raise ValueError("reviewer must be independent of producer")
health = verified["health"][1]
# A signed continuous observation attestation must name the prior revision.
if health.get("revision") != rollback or health.get("continuous") is not True:
raise ValueError("continuous prior-revision observation required")
if health.get("synced") is not True or health.get("healthy") is not True:
raise ValueError("prior revision unhealthy")
evidence = dict(schema_version=1, identity_admitted=True, authority=AUTHORITY,
checks="pass", review_result="pass", producer=verified["build"][0],
reviewer=verified["review"][0], candidate_commit=candidate,
rollback_commit=rollback, healthy_since=health["healthy_since"],
observed_at=health["issued_at"], argo_synced=True, argo_healthy=True)
validate(before, after, evidence, now)
required = sorted({c["image"] for docs in (before, after) for d in docs
if d["kind"] == "Deployment"
for c in d["spec"]["template"]["spec"]["containers"]})
if sorted(verified["retention"][1].get("images", [])) != required:
raise ValueError("live and rollback image retention coverage required")
checks = verified["build"][1].get("checks", {})
if not isinstance(checks, dict) or any(checks.get(name) != "success" for name in REQUIRED_CHECKS):
raise ValueError("required build and smoke checks missing")
# Signed build receipt binds each exact candidate digest, not just a green status.
candidate_images = sorted({c["image"] for d in after if d["kind"] == "Deployment"
for c in d["spec"]["template"]["spec"]["containers"]})
if sorted(verified["build"][1].get("images", [])) != candidate_images:
raise ValueError("build digest binding missing")
plan = {**binding, "receipts": envelopes, "receipt_hashes": {r: fingerprint(envelopes[r]) for r in ROLES}}
release_id = fingerprint(binding)
db = self.connect()
try:
db.execute("BEGIN IMMEDIATE")
existing = db.execute("SELECT id FROM releases WHERE id=?", (release_id,)).fetchone()
if existing:
db.rollback()
return release_id
db.execute("INSERT INTO releases VALUES (?, ?, 'planned', 1, ?, NULL)",
(release_id, canonical(plan).decode(), min(timestamp(verified[r][1]["expires_at"]) for r in ROLES).isoformat()))
db.execute("INSERT INTO transitions(release_id,phase,observed_at) VALUES (?, 'planned', ?)", (release_id, now.isoformat()))
db.commit()
finally:
db.close()
return release_id
def advance(self, release_id, backend: Backend, now=None):
"""One durable step. Adapter exceptions preserve intent for safe retry.
Failure/timeout after publication moves to rollback; rollback failure keeps
the unique active slot, halting all new releases until recovery succeeds.
A database write lock serializes adapter calls across broker processes.
"""
if not self.admitted:
raise ValueError("broker identity not admitted")
now = now or datetime.now(timezone.utc)
db = self.connect()
try:
db.execute("BEGIN IMMEDIATE")
row = db.execute("SELECT plan, phase, deadline FROM releases WHERE id=?", (release_id,)).fetchone()
if row is None:
raise ValueError("unknown release")
plan, phase, deadline = json.loads(row[0]), row[1], timestamp(row[2])
candidate, rollback = plan["candidate"], plan["rollback"]
if phase in TERMINAL:
db.rollback()
return phase
next_phase = phase
if phase == "planned":
if now >= deadline:
# No publication attempted; safe terminal cancellation.
next_phase = "cancelled"
else:
# Persist intent BEFORE the external CAS; a lost response
# must never be mistaken for an unpublished cancellation.
next_phase = "publish_pending"
elif phase == "publish_pending":
if now >= deadline:
next_phase = "rollback_planned"
else:
backend.publish_revision(rollback, candidate)
next_phase = "published"
elif phase == "published":
if now >= deadline:
next_phase = "rollback_planned"
else:
backend.sync_revision(candidate)
next_phase = "synced"
elif phase == "synced":
next_phase = "complete" if now < deadline and backend.healthy(candidate) is True else "rollback_planned"
elif phase == "rollback_planned":
backend.publish_revision(candidate, rollback)
next_phase = "rollback_published"
elif phase == "rollback_published":
backend.sync_revision(rollback)
next_phase = "rollback_synced"
elif phase == "rollback_synced":
if backend.healthy(rollback) is True:
next_phase = "rolled_back"
else:
raise ValueError("unknown durable phase")
db.execute("UPDATE releases SET phase=?, active=? WHERE id=?",
(next_phase, None if next_phase in TERMINAL else 1, release_id))
if next_phase != phase:
db.execute("INSERT INTO transitions(release_id,phase,observed_at) VALUES (?, ?, ?)", (release_id, next_phase, now.isoformat()))
db.commit()
return next_phase
except Exception:
db.rollback()
raise
finally:
db.close()

View file

@ -0,0 +1,63 @@
"""Fixed mutation shapes for the trusted Git/ArgoCD release adapter.
These builders confer no authority. The admitted adapter must authenticate its
transport, enforce compare-and-swap, and never accept arbitrary path/body inputs.
"""
import copy
import re
import yaml
REVISION = re.compile(r'[0-9a-f]{40}')
APPLICATION_PATH = 'argocd/railiance01/applications/activity-core.application.yaml'
ROOT_APPLICATION = 'railiance-apps-root'
APPLICATION = 'activity-core'
NAMESPACE = 'argocd'
def revision(value):
if not isinstance(value,str) or REVISION.fullmatch(value) is None:
raise ValueError('full commit revision required')
return value
def update_child(document, expected, target):
"""Change only targetRevision in the one validated child Application."""
revision(expected);revision(target)
before = yaml.safe_load(document)
if not isinstance(before,dict):raise ValueError('invalid child declaration')
spec=before.get('spec',{});source=spec.get('source',{})
if (before.get('apiVersion')!='argoproj.io/v1alpha1' or before.get('kind')!='Application'
or before.get('metadata',{}).get('name')!=APPLICATION
or before.get('metadata',{}).get('namespace')!=NAMESPACE
or spec.get('project')!='activity-core'
or source.get('repoURL')!='https://forgejo.coulomb.social/coulomb/activity-core.git'
or source.get('path')!='k8s/gitops'
or spec.get('destination')!={'namespace':'activity-core','server':'https://kubernetes.default.svc'}
or spec.get('syncPolicy',{}).get('automated') is not None
or set(spec.get('syncPolicy',{}).get('syncOptions',[])) != {'CreateNamespace=false','ApplyOutOfSyncOnly=true','PruneLast=true','FailOnSharedResource=true'}
or 'sources' in spec or before.get('metadata',{}).get('finalizers')):
raise ValueError('child outside release contract')
if source.get('targetRevision') not in {expected,target}:
raise ValueError('child revision CAS conflict')
if source['targetRevision']==target:return document
# Preserve comments and all other bytes; ambiguous duplicate YAML keys cannot
# silently widen this one-field text patch.
pattern=re.compile(r'(?m)^ targetRevision: '+re.escape(expected)+r'[ \t]*$')
if len(pattern.findall(document))!=1:raise ValueError('ambiguous child revision field')
updated=pattern.sub(' targetRevision: '+target,document)
wanted=copy.deepcopy(before);wanted['spec']['source']['targetRevision']=target
if yaml.safe_load(updated)!=wanted:raise ValueError('unexpected child mutation')
return updated
def sync_operations(platform_revision, activity_revision):
"""Return fixed root-selective and child sync bodies, never prune/overrides."""
revision(platform_revision);revision(activity_revision)
def body(value,resources=None):
sync={'revision':value,'prune':False,'syncStrategy':{'apply':{}}}
if resources is not None:sync['resources']=resources
return {'operation':{'initiatedBy':{'username':'activity-core-release-broker'},'sync':sync}}
return [
(ROOT_APPLICATION,body(platform_revision,[{'group':'argoproj.io','kind':'Application','name':APPLICATION,'namespace':NAMESPACE}])),
(APPLICATION,body(activity_revision)),
]

View file

@ -0,0 +1,202 @@
"""Isolated failure proofs: generated test keys and an in-memory deployment adapter."""
import base64
import copy
import json
import sqlite3
from datetime import datetime, timedelta, timezone
from pathlib import Path
import pytest
import yaml
from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat
from activity_core.release_broker import AUTHORITY, REQUIRED_CHECKS, Broker, Receipts, canonical, fingerprint
ROOT = Path(__file__).resolve().parents[1]
NOW = datetime(2026, 9, 28, 16, tzinfo=timezone.utc)
@pytest.fixture
def bundle(tmp_path):
before = list(yaml.safe_load_all((ROOT / 'k8s/gitops/runtime.yaml').read_text()))
after = copy.deepcopy(before)
next(d for d in after if d['metadata']['name'] == 'actcore-worker')['spec']['template']['spec']['containers'][0]['image'] = 'forgejo.coulomb.social/coulomb/activity-core@sha256:'+'f'*64
keys = {r: Ed25519PrivateKey.generate() for r in ['build','review','health','retention']}
trusted = {r: {'public_key': k.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw),
'principal':r, 'roles':{r}} for r,k in keys.items()}
broker = Broker(tmp_path/'release.sqlite', Receipts(trusted), admitted=True)
return broker, before, after, keys
def images(docs):
return {c['image'] for d in docs if d['kind']=='Deployment' for c in d['spec']['template']['spec']['containers']}
def envelopes(before, after, keys, candidate='a'*40, rollback='b'*40):
binding = dict(repository='coulomb/activity-core',application='activity-core',
candidate=candidate,rollback=rollback,before_sha256=fingerprint(before),after_sha256=fingerprint(after))
out={}
for role,key in keys.items():
payload=dict(schema=1,role=role,authority=AUTHORITY,binding=binding,result='pass',
issued_at=NOW.isoformat(),expires_at=(NOW+timedelta(minutes=5)).isoformat())
if role=='health':payload.update(revision=rollback,continuous=True,healthy=True,synced=True,healthy_since=(NOW-timedelta(hours=25)).isoformat())
if role=='build':
payload['images']=sorted(images(after))
payload['checks']={name:'success' for name in REQUIRED_CHECKS}
if role=='retention':payload['images']=sorted(images(before)|images(after))
out[role]={'key_id':role,'payload':payload,'signature':base64.b64encode(key.sign(canonical(payload))).decode()}
return out
def resign(receipt,key):
receipt['signature']=base64.b64encode(key.sign(canonical(receipt['payload']))).decode()
def admit(bundle, **kwargs):
broker,before,after,keys=bundle
return broker.admit(before,after,'a'*40,'b'*40,envelopes(before,after,keys),now=NOW,**kwargs)
class FakeBackend:
def __init__(self):
self.revision='b'*40;self.calls=[];self.fail_health=False;self.fail_rollback=False;self.lose_response=False
def publish_revision(self,expected,target):
if self.revision not in {expected,target}:raise ValueError('CAS conflict')
self.revision=target;self.calls.append(('publish',target))
if self.lose_response:
self.lose_response=False
raise ConnectionError('lost response after durable publication')
def sync_revision(self,target):
assert self.revision==target
self.calls.append(('sync',target))
def healthy(self,target):
assert self.revision==target
return not (self.fail_health if target=='a'*40 else self.fail_rollback)
def drive(broker, rid, backend, now=NOW):
phases=[]
for _ in range(9):
phase=broker.advance(rid,backend,now);phases.append(phase)
if phase in {'complete','rolled_back','cancelled'}:break
return phases
def test_normal_release_restart_and_idempotent_delivery(bundle):
broker,*_=bundle;rid=admit(bundle);backend=FakeBackend()
assert broker.advance(rid,backend,NOW)=='publish_pending'
assert not backend.calls
restarted=Broker(broker.database,broker.receipts,admitted=True)
assert drive(restarted,rid,backend)==['published','synced','complete']
assert admit(bundle)==rid
previous=list(backend.calls)
assert restarted.advance(rid,backend,NOW)=='complete'
assert backend.calls==previous
with broker.connect() as db:
assert [r[0] for r in db.execute('SELECT phase FROM transitions ORDER BY sequence')]==['planned','publish_pending','published','synced','complete']
def test_health_failure_rolls_back_through_same_adapter(bundle):
broker,*_=bundle;rid=admit(bundle);backend=FakeBackend();backend.fail_health=True
assert drive(broker,rid,backend)[-1]=='rolled_back'
assert backend.calls==[('publish','a'*40),('sync','a'*40),('publish','b'*40),('sync','b'*40)]
def test_failed_rollback_holds_exclusive_slot(bundle):
broker,before,after,keys=bundle;rid=admit(bundle);backend=FakeBackend();backend.fail_health=True;backend.fail_rollback=True
assert drive(broker,rid,backend)[-1]=='rollback_synced'
with pytest.raises(sqlite3.IntegrityError):
broker.admit(before,after,'c'*40,'b'*40,envelopes(before,after,keys,candidate='c'*40),now=NOW)
backend.fail_rollback=False
assert broker.advance(rid,backend,NOW)=='rolled_back'
def test_lost_publish_response_and_expiry_recovers_prior_revision(bundle):
broker,*_=bundle;rid=admit(bundle);backend=FakeBackend();backend.lose_response=True
assert broker.advance(rid,backend,NOW)=='publish_pending'
with pytest.raises(ConnectionError):broker.advance(rid,backend,NOW)
assert backend.revision=='a'*40
restarted=Broker(broker.database,broker.receipts,admitted=True)
assert drive(restarted,rid,backend,NOW+timedelta(minutes=6))[-1]=='rolled_back'
assert backend.revision=='b'*40
def test_expired_plan_never_publishes(bundle):
broker,*_=bundle;rid=admit(bundle);backend=FakeBackend()
assert broker.advance(rid,backend,NOW+timedelta(minutes=6))=='cancelled'
assert backend.calls==[]
@pytest.mark.parametrize('kind',['signature','commit','stale','future','key','role','scope','retention','build','independence','health','checks'])
def test_rejects_untrusted_or_incomplete_evidence(bundle,kind):
broker,before,after,keys=bundle;e=envelopes(before,after,keys);r=e['review']
if kind=='signature':r['payload']['result']='fail'
elif kind=='commit':r['payload']['binding']['candidate']='c'*40;resign(r,keys['review'])
elif kind=='stale':r['payload']['issued_at']=(NOW-timedelta(minutes=6)).isoformat();resign(r,keys['review'])
elif kind=='future':r['payload']['issued_at']=(NOW+timedelta(seconds=1)).isoformat();resign(r,keys['review'])
elif kind=='key':r['key_id']='caller-provided'
elif kind=='role':r['key_id']='build'
elif kind=='scope':r['payload']['authority']='admin';resign(r,keys['review'])
elif kind=='retention':e['retention']['payload']['images']=[];resign(e['retention'],keys['retention'])
elif kind=='build':e['build']['payload']['images']=[];resign(e['build'],keys['build'])
elif kind=='independence':broker.receipts.trusted['review']['principal']='build'
elif kind=='checks':e['build']['payload']['checks']={};resign(e['build'],keys['build'])
elif kind=='health':e['health']['payload']['continuous']=False;resign(e['health'],keys['health'])
with pytest.raises((ValueError,InvalidSignature)):
broker.admit(before,after,'a'*40,'b'*40,e,now=NOW)
with broker.connect() as db:assert db.execute('SELECT count(*) FROM releases').fetchone()[0]==0
def test_signed_out_of_scope_manifest_still_refused(bundle):
broker,before,after,keys=bundle
next(d for d in after if d['kind']=='Deployment')['spec']['replicas']=99
with pytest.raises(ValueError,match='non-image'):
broker.admit(before,after,'a'*40,'b'*40,envelopes(before,after,keys),now=NOW)
def test_unadmitted_identity_cannot_start_or_resume(bundle):
broker,*_=bundle;rid=admit(bundle);broker.admitted=False
with pytest.raises(ValueError,match='not admitted'):admit(bundle)
with pytest.raises(ValueError,match='not admitted'):broker.advance(rid,FakeBackend(),NOW)
def test_fixed_git_and_argo_mutation_shapes():
from activity_core.release_operations import update_child, sync_operations
declaration = '''apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: activity-core
namespace: argocd
spec:
project: activity-core
source:
repoURL: https://forgejo.coulomb.social/coulomb/activity-core.git
targetRevision: BBBB
path: k8s/gitops
destination:
namespace: activity-core
server: https://kubernetes.default.svc
syncPolicy:
syncOptions: [CreateNamespace=false, ApplyOutOfSyncOnly=true, PruneLast=true, FailOnSharedResource=true]
'''.replace('BBBB','b'*40)
updated=update_child(declaration,'b'*40,'a'*40)
assert updated==declaration.replace('b'*40,'a'*40)
assert update_child(updated,'b'*40,'a'*40)==updated
with pytest.raises(ValueError,match='CAS'):update_child(declaration,'c'*40,'a'*40)
with pytest.raises(ValueError):update_child(declaration.replace('namespace: activity-core','namespace: foreign'),'b'*40,'a'*40)
with pytest.raises(ValueError):update_child(declaration,'b'*40,'main')
root,child=sync_operations('c'*40,'a'*40)
assert root[0]=='railiance-apps-root' and child[0]=='activity-core'
assert root[1]['operation']['sync']['resources']==[{'group':'argoproj.io','kind':'Application','name':'activity-core','namespace':'argocd'}]
assert all(op['operation']['sync']['prune'] is False for _,op in [root,child])
def test_different_principals_cannot_share_review_signing_key(bundle):
broker,before,after,keys=bundle
e=envelopes(before,after,keys)
broker.receipts.trusted['review']['public_key']=broker.receipts.trusted['build']['public_key']
resign(e['review'],keys['build'])
with pytest.raises(ValueError,match='independent'):
broker.admit(before,after,'a'*40,'b'*40,e,now=NOW)

165
uv.lock generated
View file

@ -8,6 +8,7 @@ source = { editable = "." }
dependencies = [ dependencies = [
{ name = "alembic" }, { name = "alembic" },
{ name = "asyncpg" }, { name = "asyncpg" },
{ name = "cryptography" },
{ name = "fastapi" }, { name = "fastapi" },
{ name = "httpx" }, { name = "httpx" },
{ name = "nats-py" }, { name = "nats-py" },
@ -29,6 +30,7 @@ dev = [
requires-dist = [ requires-dist = [
{ name = "alembic", specifier = ">=1.14" }, { name = "alembic", specifier = ">=1.14" },
{ name = "asyncpg", specifier = ">=0.29" }, { name = "asyncpg", specifier = ">=0.29" },
{ name = "cryptography", specifier = ">=44.0" },
{ name = "fastapi", specifier = ">=0.115" }, { name = "fastapi", specifier = ">=0.115" },
{ name = "httpx", specifier = ">=0.27" }, { name = "httpx", specifier = ">=0.27" },
{ name = "nats-py", specifier = ">=2.7" }, { name = "nats-py", specifier = ">=2.7" },
@ -144,6 +146,104 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/22/30/7cd8fdcdfbc5b869528b079bfb76dcdf6056b1a2097a662e5e8c04f42965/certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a", size = 135707 }, { url = "https://files.pythonhosted.org/packages/22/30/7cd8fdcdfbc5b869528b079bfb76dcdf6056b1a2097a662e5e8c04f42965/certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a", size = 135707 },
] ]
[[package]]
name = "cffi"
version = "2.1.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pycparser", marker = "implementation_name != 'PyPy'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/9e/ef/008a1939e372c06329a3fce4279c02f328488f3526744906eeec3da7ad5f/cffi-2.1.1.tar.gz", hash = "sha256:dd31f52ea1086513bb9df30f8fcee9b8918323ae067a3d5b78bc826a000712be", size = 530807 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/70/d2/16d99a0c4948febc0ebd133a13b2f688ff7f8cb04da971e1128872ce0c03/cffi-2.1.1-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:c8d2c9fd1f2d16f780d15127abb050d13d1a76c03a4bd87d7e4980e45e511e12", size = 183838 },
{ url = "https://files.pythonhosted.org/packages/cd/95/31b535a9f0220ae9f357de4a08d57ce89cb417653c2fd9f075f50822a388/cffi-2.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:398aff33cee2767e3e781d2554c54bd0dff386bb437581e0d8011fde1a942ec1", size = 184168 },
{ url = "https://files.pythonhosted.org/packages/ad/5a/4707a0dc1f203f5dde5a907b0d4e3c25d71120241048bd5bc6f1bb9d4e71/cffi-2.1.1-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:154852545011f779917b11c78db2358d095da62a9a172b78ad0a583ee5adc0d0", size = 211805 },
{ url = "https://files.pythonhosted.org/packages/ad/66/c19feabb28485b6e0bbaaafa90837a1ef5d302e90f2178bd33f17a49879b/cffi-2.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3311ed60d36f83378794e1009ac6258bafbf81f7888b4caa7b35a521e3f95813", size = 218716 },
{ url = "https://files.pythonhosted.org/packages/a7/92/500760486c8baab49a7a8a58ba7fc3355ec3974b454b8a09e528efde9e1d/cffi-2.1.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6e192623c49c94421616a5778fba35cf0d5a8d000650c1967ef4448ee5cdd990", size = 205569 },
{ url = "https://files.pythonhosted.org/packages/a5/a7/a67c733254d6e7373f7822f8082d8d6beade791e0cf12a7611f376fa61c7/cffi-2.1.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a6e721d4b0e45d5b65e87534470e67b18dcd092c83f68fba09f152b9cbc061af", size = 204907 },
{ url = "https://files.pythonhosted.org/packages/f7/a4/4399daaf8f7dfee9d7c3327fdb0426ee041cc63edc358b93911ceb2bfc7a/cffi-2.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:34e261f78cb6ceaaa36f42f2613f4380d94d9c759a9c73c769ee6e0247364632", size = 217807 },
{ url = "https://files.pythonhosted.org/packages/28/f7/dabe6da2466ecbd82dc62e7342dc6b1065dad990c06f00f0ede9ebf2a0ed/cffi-2.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7225e4514edb64eb6740324353e0da0711954fd8d7da4576755b1c6e09b697cd", size = 221252 },
{ url = "https://files.pythonhosted.org/packages/ce/87/616202d8e51342c07d2534c510111c4cc37201775ce8f60802c9335d1edd/cffi-2.1.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:df913725b79db7bcf03448f36b7bf8815363417d5b58deecf9305e3e30f0f21a", size = 214214 },
{ url = "https://files.pythonhosted.org/packages/b4/c6/ab025d75d2c26c19b087c0124e75ee31cb65032f4fe345d356d8c507ab97/cffi-2.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f5cfbc5fe74540d335175b656c725d74d90e3730c626d92575eea35029d9afaa", size = 219408 },
{ url = "https://files.pythonhosted.org/packages/db/e2/7e8109f65445bdc673a7b54f02c677de462db75674220fd1335efc8eb598/cffi-2.1.1-cp311-cp311-win32.whl", hash = "sha256:f8ec5e643a9a937f64e1999eb9f75d072263751912dc5cd06d3c85f8f44be7c3", size = 174470 },
{ url = "https://files.pythonhosted.org/packages/73/c0/77ba02423c2f7d7091143c45cd49e0e6575c4c1967394bb542bd923a9b74/cffi-2.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:42f6930c31dc7f50732c9ae793c2786c7b6b044195967bbdde40bb9be81c4cc0", size = 185096 },
{ url = "https://files.pythonhosted.org/packages/7c/47/9f1f85f9672ceda4984dc6c4f8824e8558992a2972c3d3c81fb8eb28d4ba/cffi-2.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:c7659f22557c5a0bc4855cd635f55edec690cc008a40768527762cb9fb263455", size = 179941 },
{ url = "https://files.pythonhosted.org/packages/10/69/43965eccfdead3b9220015fd1320e117be8c6ed01a62ffab76eeb752f5d5/cffi-2.1.1-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:c8c69575568085ba0b1b10c0249d779a214aea6f6522e949a0fc9fb0fcb449d0", size = 184821 },
{ url = "https://files.pythonhosted.org/packages/54/7d/16e5a096677b5e313ca80cd5e5170efa3ea44624a82bb111925522da64b1/cffi-2.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f81b3b8f3d4e343550fa4baa0e479bba9f2d29ce9c2e9b51d1ce1718d7442fcf", size = 184719 },
{ url = "https://files.pythonhosted.org/packages/56/e6/8941622732edec876dd17d0453dce07317ae96db34f2ec1436c9d3785986/cffi-2.1.1-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:811bd1e21d32de12efca32393a0ab3f5133b54fce9bd44b8bd77ab07da14bf6a", size = 214799 },
{ url = "https://files.pythonhosted.org/packages/44/de/f98430906df1545ffde0d543dd124a7a439bc2cd32b36b9c53f805df7333/cffi-2.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:68e62fe11f30d5ca8289242866f0a5291402d8529ca2178ab8afc5c9694ae890", size = 222389 },
{ url = "https://files.pythonhosted.org/packages/6a/5b/717f1526b9957b34456313c31645c5b82b8fb5c3fe9e4752999be7128bfc/cffi-2.1.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:4a7c934f7360e8cd64fe9efadcbd10c7c6364f531e432b9a4bf5ccbc9e0e8b50", size = 210249 },
{ url = "https://files.pythonhosted.org/packages/64/b3/f8aa4f3e34986c7e4ec45072d1b1b9dd295b6b18007b45518d79726dd725/cffi-2.1.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:3143d81e29e1e20a9ce10901ec369012947876596f75a222235965f2b7ae832e", size = 208775 },
{ url = "https://files.pythonhosted.org/packages/b1/db/dceb9dd5b231e1da801793f8acc9f3c52a7e1afe40bb1aae37e02b0faad5/cffi-2.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c1453022f490d2459a11819d83ad1d586e9ff65a12ac3e705ffebd46d3685dcf", size = 221822 },
{ url = "https://files.pythonhosted.org/packages/a0/d2/6cd24ae3be000a634109c247d1475d62e5616d0dc78c82770942ec384248/cffi-2.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:208f941bb9d18e768138677f0a6d2ce01f590df56043dda1df1535ac57c88517", size = 225232 },
{ url = "https://files.pythonhosted.org/packages/cb/52/3fa190537004dd7f0ab860a6dc7c0175b8667f68d1e618a46f5498d30250/cffi-2.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:210019b6c7cf07f081b4c54635c8cf744377001350e29cc0f81c4377b4797735", size = 223597 },
{ url = "https://files.pythonhosted.org/packages/80/fb/0bb75b7039588c074b37ae99f40d9bfddf990ecb2fbc346ebccd2e56b9be/cffi-2.1.1-cp312-cp312-win32.whl", hash = "sha256:046bfc24911b37851ee1b51aab8bffe713d89c68c6a057b09484ce9fd5f69b4e", size = 175292 },
{ url = "https://files.pythonhosted.org/packages/d9/79/615cc094e2fb508cade7de88d3b4f6c4ec2bab695c97bce9153dc65aadf5/cffi-2.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:f53e442b08449d42821fa4a4fba000095af9f62742a500f978a9f557ec44339a", size = 185919 },
{ url = "https://files.pythonhosted.org/packages/70/c6/d0ea84713fe46b243a436a18fcd47d639732747e21635c8a27191b06dc30/cffi-2.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:7bde5e4cc5c10140859842b9d383af292b22639a4dffb725314baf45968cef80", size = 180093 },
{ url = "https://files.pythonhosted.org/packages/9d/f4/035513d4117049066b4779dc3b7c0c0fdad175fa13731c9f4003f1cd1478/cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:b5bdfd1c873d4e093aabc0ca84c4ca6dbc4f752afb5c86f146d9742580c9da2e", size = 194248 },
{ url = "https://files.pythonhosted.org/packages/76/af/2aeb4dbb5fc41a04161ae9ff1518de7cec08e164f44a8ce6a4cf7fd2cd1d/cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:31348097ff5bbe827ccc41795d4dd099d9f0625e7def00ee653c137a490c2a6c", size = 196908 },
{ url = "https://files.pythonhosted.org/packages/a7/46/2e5fdde8555706dd98139a910ca11be02809f3f605ce956f655d0214e100/cffi-2.1.1-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:9d2055050ea716bd38b7f7f1579c275386646b4894c155a3e2f3cd62ed41b7c6", size = 184805 },
{ url = "https://files.pythonhosted.org/packages/55/41/4c7042f317b9217502988f0873af87e16ad606dc20f84e546e3e6ce9764c/cffi-2.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:19ee6127ee34de7d83ce3d371ebc5ed91addbdcc39f9ab15ce4eb35a4e534971", size = 184764 },
{ url = "https://files.pythonhosted.org/packages/43/1f/1c3d90d91811c8f86ced9ed637956c54bfe5b79ca98fe976d7f8c8979f6b/cffi-2.1.1-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:6a8dddef476fab96d066d578fc88526767b836ab5ab21754e1d5bf3879c31c7c", size = 214722 },
{ url = "https://files.pythonhosted.org/packages/37/6f/3b5ce4c3b2192d250f04908f2bfd91ef34552ec8f7716a5d4abdb8d67bb2/cffi-2.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f16c709686a78c727bbbf059f92b0bf41c6fc60deec706d2dc19f529175a6125", size = 222369 },
{ url = "https://files.pythonhosted.org/packages/02/10/4b3c75dde3d9663c9e02ba05c2668b954f671d4bbe346413ca8c696b295a/cffi-2.1.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:fcd22650c908d7b7da162bbfaab594a1227a15d1643a98c68b122ac642fa2264", size = 210175 },
{ url = "https://files.pythonhosted.org/packages/df/62/14f74b9543e605d17701dc797b815958b8bb70b7624ce1b832ddad48ed6c/cffi-2.1.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:aa9511c62d14da7aacc9b4bf51f3f697a621e83b2d6919008243c3aad168eea3", size = 208670 },
{ url = "https://files.pythonhosted.org/packages/95/95/86342356ff5953b3fb06f7ef7c5bee212d45e770abc7218d451b9148313c/cffi-2.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a931079504ecc49efed7744c476a5c343a92fabf66dec2db95edb1b2fdc770e2", size = 221824 },
{ url = "https://files.pythonhosted.org/packages/eb/ff/7b3429ff53aafe931ed8a5fc69f481bbef7ba6de87ddcbb63d08f483f613/cffi-2.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a2d7755bef5a12ed488f4ef1f1b69ee9191d7396083b755a5d2295f6edb4768b", size = 225148 },
{ url = "https://files.pythonhosted.org/packages/34/34/a95870b9221e09cf4f2ce3178b1a210abdfe63a1bd357da940418d7b8d15/cffi-2.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e0bcb7e0f677f543555d2adff3bf19c05f66cdb4796e5ff602442ab2fe3c4ef7", size = 223564 },
{ url = "https://files.pythonhosted.org/packages/70/ea/839b50531021a647fb5e929f72cf97bc1ff702b5472166164b5b6e76b851/cffi-2.1.1-cp313-cp313-win32.whl", hash = "sha256:334644fbac4eff73d985a17a91226df55d0f394160c4cfb880e084c8f7161cac", size = 175263 },
{ url = "https://files.pythonhosted.org/packages/60/a6/8b149b2c3f2e11aaa1618ef64500b45f50f22c57a977a4dff1aff1f91042/cffi-2.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:1aa5645c30469b09530c4ebca77ebf8f17618293c58f8549cb1a543a50236e7d", size = 185688 },
{ url = "https://files.pythonhosted.org/packages/01/9a/11f687cb39d6a3504060d5242f04f48c735afb4d3d533958a20594890cb2/cffi-2.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:63bbfd5ded17c4840ac07cd8f1c21ba9d9708141f840b324f422f41b207e3973", size = 180078 },
{ url = "https://files.pythonhosted.org/packages/d3/7b/d6bbf82b8b96e7391438898c42f5bd96dd02030fd5b64937d248220003e2/cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7dbb61fe3a7699468030f71bbe5f8a0e326a151daa91beb11a6fc1f980c55e1c", size = 194064 },
{ url = "https://files.pythonhosted.org/packages/94/e6/bcc91b283be94735e268487a054004f0aa19947b6348fa367db53230abc8/cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:f24fb43132a4c6b4cb4eb029492919b2db645be6808d738f244fd146c03c32cb", size = 196720 },
{ url = "https://files.pythonhosted.org/packages/d9/99/c4b0c17cacdc9c3b8f280026286a9826d6a208c0f047591a3c3ce99b91fd/cffi-2.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d28630f5854ab07ab1fd4aba756de52326c82e6be15d414b12793f1975048b54", size = 184964 },
{ url = "https://files.pythonhosted.org/packages/b3/a9/9db617d05d7367c1ad0ab00b3aa6e6f9281edd689b4ee9ea0e5a84e89c97/cffi-2.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:661c298b4821edebead0c91edd2b00374d67ad7c5a1f7a91d4442633b79d6a72", size = 184962 },
{ url = "https://files.pythonhosted.org/packages/67/b8/b42132ca113dc567d37684437b46ca1dafc885902b02a110a02d5b511857/cffi-2.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:58acb8ab8e295e6c5ea12f888cbb13cf21511ef2a3303a23f4325c29d17fe5c1", size = 222328 },
{ url = "https://files.pythonhosted.org/packages/80/10/c5c0cbf0a657aecf59ef511409734230bf556f05a0d6c9eed7aa5c0a0166/cffi-2.1.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:456a61fa52d579ebf9df2e9552ead5129855dbaff6c1e5a9b1bc408809bdc062", size = 209985 },
{ url = "https://files.pythonhosted.org/packages/d5/6c/bfa0b87b03b9238148beca990292843c9396ba069b54496596594173de7b/cffi-2.1.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a4f00aa42f75d6e4595e8866e748cc1705adc0cddfeb2ca86d0d03993d63ba03", size = 208530 },
{ url = "https://files.pythonhosted.org/packages/e9/02/4e7d553a7ac4b4238b38b3c1b80d486e9d4436f8d2acbf87a0997fe3f402/cffi-2.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b0431303acaea1089ad4b3e9ce4e6518193def1118d4073ca848635ee4ea2e96", size = 221525 },
{ url = "https://files.pythonhosted.org/packages/82/1d/a4aaf9babd75acb4d5f223bff71533bee748dd770a382619a798960ee9ba/cffi-2.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:64faea20f4e2613363a1a9b9c7dd73058f3ecd00133a511e72ad7c511658f527", size = 225053 },
{ url = "https://files.pythonhosted.org/packages/81/10/5dc0e7bdd18e22107054288283380fc97a06ae3f1656a106908d666a3c88/cffi-2.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5c58fe613dc5e5336357eff555824a314d8e43282600435c8d1cb6a7a2fedd13", size = 223213 },
{ url = "https://files.pythonhosted.org/packages/0b/e9/d0061c364cde06ee43168a0d076ac1da512cbc380d44767b844ba34fe2b6/cffi-2.1.1-cp314-cp314-win32.whl", hash = "sha256:1a18a57b58cfb21fc28d72e876acf10eaed67a1ed96226f92af4df681d571c4c", size = 177682 },
{ url = "https://files.pythonhosted.org/packages/a7/06/1c3e01e3ba14c39f6d10bfbac52753b7e22259e38088e5cfe1d704918690/cffi-2.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:3222ba5d678f80a030e6afbcc33dc1ae5cb45facabb61cee2c7016b8432fde48", size = 187949 },
{ url = "https://files.pythonhosted.org/packages/87/5b/da4e39efe18eeb89cf580ea9cfc66b6a7c3eadb808fc0cc1d3a295cb5a5d/cffi-2.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:ab36d55f9ed2d067327667c2fea18dda018eb628dd6347aa01dda6cf1f5d3836", size = 182947 },
{ url = "https://files.pythonhosted.org/packages/23/59/40338bf421c5accea1d45158170c87006ef1cd371b05c077e76476949728/cffi-2.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7750c6449dff7864bb9bb27ddfb0267756189201a3afc911d82b3caacd70dfc3", size = 188504 },
{ url = "https://files.pythonhosted.org/packages/7d/47/5ecf1023850036e674c77ec4de86182d309ae344e39e7cba984b7df5d647/cffi-2.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0beceaabe56af686895136a2de78db54ecd8e4046b236b8fd6d6cb61389e9bf2", size = 188259 },
{ url = "https://files.pythonhosted.org/packages/2a/9c/92934c3bea9f785b23eba304538c0b4d37a2a96d2431eb3a1bc87a11aa19/cffi-2.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:49cbc70e6542d4ccccb936558d1064a8012541e78f821f955cff24e357776c94", size = 223864 },
{ url = "https://files.pythonhosted.org/packages/4d/45/ba4c93527bc38616a8bd36488acb69a2212d60486794f0c1f318949bbb76/cffi-2.1.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e2d65b31f36619cda3999b78b2aa9632e76b78448e7a56fc4240824200e7c4fc", size = 211538 },
{ url = "https://files.pythonhosted.org/packages/80/e9/b6ef565e452acb932fb0cb5443f44a78efbd1233e566f02b5a83855e9115/cffi-2.1.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:28907ab9bfb6aa13184cfc17c6b8e1023c5ab6fd7076d8c20a35e59fe04f8f29", size = 210688 },
{ url = "https://files.pythonhosted.org/packages/9a/95/eff5f0cee78d2eabc7eebffec40d3fc1876b5f3c95582e018bb4b99601f2/cffi-2.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:51b31d1c98274844cfd7838ce00bfc27c7423a4dc00fc0772fc3331c2cc90676", size = 223803 },
{ url = "https://files.pythonhosted.org/packages/fa/01/579d39fb8bef00a335a23d83757b44feb24cd6345a2c451b64cb67b9c362/cffi-2.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e7cecbaadb83884793e05828cee59b210b24583b9c7425d0ba6a754fe22eb4e", size = 226763 },
{ url = "https://files.pythonhosted.org/packages/8d/b0/0b44f47c60b01b57b6e2bbd92343f13a85a1d93bc46ccf6e47e244acd99c/cffi-2.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:25792eac27877609e7bb06d42ff88278a6624fff2ba9bbb523c09616b117e80f", size = 225688 },
{ url = "https://files.pythonhosted.org/packages/eb/d2/3b7176cb570a1d3e27faf67b72f591af508036e0d8b2be2ef9af9e8c84bb/cffi-2.1.1-cp314-cp314t-win32.whl", hash = "sha256:8ef53b2de9bcb9197d31854256575d59dbac0cba72ac627bb291ef5eceb74be4", size = 182868 },
{ url = "https://files.pythonhosted.org/packages/56/78/31f00c1bcd97c9bbf55f1bfdf5bc809a5de8887473e90bb9960dca825e80/cffi-2.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:616f097f2fe415bc92a247f02e11f634e1f9e9a83d327e3c915c15089c87869e", size = 194104 },
{ url = "https://files.pythonhosted.org/packages/7b/1b/58496f2ed0a35de575250c02a43ab3cc2c04d494a88fed31c1cabc0fd176/cffi-2.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ad2c86c495b899d862ea0f4b42891b8713a3bd45dd4105c7fd51c2a72f39f3a5", size = 186402 },
{ url = "https://files.pythonhosted.org/packages/c1/8f/9ebe220eab48a093d1a5a5e339ab0dc7316eef3bb04d63c42f0251b61f50/cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:dddad92b554513a31f272570678ba307fb9f618f05e3d4a5eacafff9eae03e1d", size = 194043 },
{ url = "https://files.pythonhosted.org/packages/ff/69/844bad3ece306c4782c2ecb93597035b6690d48704b803914c199da1e8b3/cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:da0e573f9f97159390c89d9f1a9e41908b66d408cc5b58d08cf3847d844c531b", size = 196737 },
{ url = "https://files.pythonhosted.org/packages/1b/8a/af668013284634733f02d683458a0728739c7d6ddb5e14cb0c20832266fe/cffi-2.1.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:fb92203a88b3d3053034db775110081c49d28be6551923805e039924093761e4", size = 184933 },
{ url = "https://files.pythonhosted.org/packages/0c/75/2f5207ff6d1a613133b23a5203cc0c2a628313b5eb3974d7956ae3c57950/cffi-2.1.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2ae64be792b8966f2c69538199728b290e34726562896df1e5dc8ffd8d8188e8", size = 185002 },
{ url = "https://files.pythonhosted.org/packages/e2/31/9e1313b0a6e30e91b3b3d3fff51ae99c857c07738e3afcce1f7334e1b7ab/cffi-2.1.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:507a24c282e0f42f8ed737cf048572cbf580468da5555764a8331735e9c736b6", size = 222271 },
{ url = "https://files.pythonhosted.org/packages/50/e3/f6234a833e6e08c7007003074723c406559eecf9b48dfc97471e5a8eb7a0/cffi-2.1.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:246fa40ce8645a614ff682e0b70f37134e460eaf93a775e0cbe3cca585a67a80", size = 209919 },
{ url = "https://files.pythonhosted.org/packages/0d/fc/5f74e293fced6edb51af3a46c4ccf6c23c9943774ecb375ddbd522c76add/cffi-2.1.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:471cee653ae88de62096552e6d24ccb4a5adb8c8c9f10b5054d0122c15bf2779", size = 208529 },
{ url = "https://files.pythonhosted.org/packages/44/16/29e6d01b388bef055ecd6ca8244b3f4d336bd09e92d5d892187b9601084e/cffi-2.1.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aeae0e330c9f6acd681f647d46cefd30c29f93e3392882e792e82080c9691399", size = 221630 },
{ url = "https://files.pythonhosted.org/packages/a4/18/fa7f1f6857d5eb88a4ca99ffcbfb7c387a287ccc154c64a73e86314745d7/cffi-2.1.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:42a494cee34437f05546455144f2b5d9ac09b1face62bcfce597d2e521066688", size = 225134 },
{ url = "https://files.pythonhosted.org/packages/e0/9f/e8e3dfa04a1b4c241f8c91faacad872b4d4efd051d49764ad4e2fd4b9fea/cffi-2.1.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:cc572dace3f60ef98d7b12ff411d20f5362feb31a0439eab0085bbfd349982d7", size = 223197 },
{ url = "https://files.pythonhosted.org/packages/f8/7e/8debeb04f1ab9fe2a6963964cd6f1aaf7192627b83926586a6a4e089c9fa/cffi-2.1.1-cp315-cp315-win32.whl", hash = "sha256:4f42141fc14250de6dde5ee7ea4432be017252d91f19c5ad043c084cea629cac", size = 177683 },
{ url = "https://files.pythonhosted.org/packages/e0/31/5158704cc474ab65c1647932e88be78dc0873f47130e253be38bcaf13d01/cffi-2.1.1-cp315-cp315-win_amd64.whl", hash = "sha256:e6e8cff14d6fb0be70a09c0bdc58096f501952d04624ebf867e0e56da2df8960", size = 187897 },
{ url = "https://files.pythonhosted.org/packages/cc/4b/b3a2da8570c704ffc0f9762cdc3ec0f02c8573798e0b5cf7f11c82bbb70f/cffi-2.1.1-cp315-cp315-win_arm64.whl", hash = "sha256:27350daa11d4f10c540e6e89dada4c54feb7256ad03e9a4dc075ebad7ba360d1", size = 182935 },
{ url = "https://files.pythonhosted.org/packages/d0/ef/5443574510a1207e6f6bc38ba6e1f1de36cb48fef07b2728bb896a21f430/cffi-2.1.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:c26608d2222fb1e94487e4a387d85f13eb55d5ed725cb25a0c589ac4ee60e7bc", size = 188464 },
{ url = "https://files.pythonhosted.org/packages/7e/ae/a56fa8c4686ad50e148fcbc8d3ae0d03915ff5c30d795058988c24118cef/cffi-2.1.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:4be96343e422f2dfcd12ab5c9f5aebe03f82f737c6bffeca6830b3875cb44aab", size = 188262 },
{ url = "https://files.pythonhosted.org/packages/53/b2/6187f46f2912276a3ae284076109cc5c8680482f11f766ccf26db4a86427/cffi-2.1.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:937c0052c05a31ca1daf18de3158eed4dbfcb9cc107adbea227728d647be701e", size = 223779 },
{ url = "https://files.pythonhosted.org/packages/8a/f6/c3ad28bd19f77047a03084424fbd4cbe997303267c14423737324be0385d/cffi-2.1.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:df423d40ee8654634421812bc3b196da3f9bd7d32929da813f8394c4348a5358", size = 211520 },
{ url = "https://files.pythonhosted.org/packages/a0/cd/ccac9013a5bd9fd764de118674ab9c805b5ca10c19270d90ee273f8b2240/cffi-2.1.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a730a083190634c65cca36ba5f489531576ebd79bcd5c8e172130f6453127231", size = 210673 },
{ url = "https://files.pythonhosted.org/packages/52/86/2976131c639aead931c5bee5aba67e4b09fbeb8018b6f282f70803f923a7/cffi-2.1.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:363e05fa78e15116c3c32c210ee36884fd6b9afa6d440e47112c3bd511d64cb6", size = 223835 },
{ url = "https://files.pythonhosted.org/packages/ac/0c/33a7aeab2f9c76918c52e084beb39c570db3588133412929e8ec06fab90b/cffi-2.1.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:770de9db11e84213beec501cfcaa013b019820ca881e03344dea5844f7876d94", size = 226705 },
{ url = "https://files.pythonhosted.org/packages/e3/26/2cde30fdde421130bfc18f70395731a6e6b2053c6a1978a5258ff04e72fa/cffi-2.1.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:7da0c5eff80f0197f3b3d1232ec5a682a9325f4ae9016a78f5f5ca35f9ced1f5", size = 225539 },
{ url = "https://files.pythonhosted.org/packages/6d/cd/a361394c94b2129d604bb846f624a8e88255a3ee33129c434a00d715e64f/cffi-2.1.1-cp315-cp315t-win32.whl", hash = "sha256:06c72bb76605a4b0cd0aad6930b69d4baf7dd5d806cfc409b824191099700e66", size = 182707 },
{ url = "https://files.pythonhosted.org/packages/9b/b5/ba2b299993c26577d529b6ae29841f9e15b9fcf004d65f423f4fcf94ade9/cffi-2.1.1-cp315-cp315t-win_amd64.whl", hash = "sha256:d9c275eaacd24aa73f94ffd6de08fc3f932424d8b6c376f4bed7cde376fe7bc3", size = 193772 },
{ url = "https://files.pythonhosted.org/packages/aa/29/35e016098c814cd93de9cd320c66b5bfba14dc6ecedd3cb518fa7c408c69/cffi-2.1.1-cp315-cp315t-win_arm64.whl", hash = "sha256:d18e5ac0f2f03f4f518d3e23db0f0cad7faa1da8620e9c09461d443bbf6e6692", size = 186360 },
]
[[package]] [[package]]
name = "click" name = "click"
version = "8.3.1" version = "8.3.1"
@ -165,6 +265,62 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 },
] ]
[[package]]
name = "cryptography"
version = "50.0.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/bb/ad/5d6702db60b1e40b41ef513b6967ff5848f307d50f8449baf1634f5908f1/cryptography-50.0.1.tar.gz", hash = "sha256:5dd9bda1c12b4162f6ff568eeb5e0ff956c28d14406e875cfe8a63a2d414ff20", size = 880381 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ba/19/797e2aaac9df6a66f1550f49979dc1b1e39ecd2077501c30efa81e8d5d67/cryptography-50.0.1-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:b8f852c65863251b9e3a1b8c150ce21e59b522dbb6a7d4bc80e680d38388e986", size = 4010153 },
{ url = "https://files.pythonhosted.org/packages/90/34/9ce9a62ed9dc82ca9fd6a34445b6904af56e5f38b3eae2ed32e49c36053d/cryptography-50.0.1-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:53e279950892dc102c6b4e52af03ae5ea92fac572a1ddab78ca73a997f62b69f", size = 4723133 },
{ url = "https://files.pythonhosted.org/packages/57/26/e6d4fc8512a51a5f9ee7bfdbfb853bce1197087df40c9ad993ad370b846f/cryptography-50.0.1-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ff838d62ec1bfce4f9ba7fa16f4a7b554cd8d0c299e6be37502161a660c84eef", size = 4712478 },
{ url = "https://files.pythonhosted.org/packages/e6/de/d3cdc2815697aae84126cbd6a030ca7b6b452e28a88b501b836bd3aa7a86/cryptography-50.0.1-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e74591e283fe6eb956416c929eb58262a719fe0311fd9054c62c3350ed8760d8", size = 4730726 },
{ url = "https://files.pythonhosted.org/packages/55/32/38c0d344b98c06d34b5df8946565a9c0d6dbf32c8e0730a7f05f0a3c6cab/cryptography-50.0.1-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5fe002589592ed749ce77fe0695fcbd3500dd61d7d6db5858a7544c612fa8e45", size = 5353524 },
{ url = "https://files.pythonhosted.org/packages/e1/1b/82f0f0d8858d4432be1af790477edf62aef90324041aa07c57e57bef1af7/cryptography-50.0.1-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:51593d180cf6d179bde5c5d065bed81386b1f381656ae7d042b7ffc87a9895ad", size = 4746720 },
{ url = "https://files.pythonhosted.org/packages/29/ba/042ca458b8c64348c768284b5d23e69b92ed53d057ab779fee628564676d/cryptography-50.0.1-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:359e62deae718bce96170e223fdcb6357e4fbd3bb7a3a75f4430763532560e49", size = 4361866 },
{ url = "https://files.pythonhosted.org/packages/39/3b/e96c1ef71edef71057c7e3c3d982ce8fda554e0c52d0cc19c18845cde3eb/cryptography-50.0.1-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e2ca8fd1b6b4b82a1c4cb02841d0837e3c12336c2e24b520ab8ab3b969733d8f", size = 4730028 },
{ url = "https://files.pythonhosted.org/packages/e3/38/45abd72ef63f2e7d0754a6cacf97bd8b69512ace7f6130d24c39ece65da2/cryptography-50.0.1-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:76de83fbd91ac49c0feaaa983d0748fd7a53176afac5fb3bf7478d244f0eb527", size = 5308405 },
{ url = "https://files.pythonhosted.org/packages/85/66/6ccca4722987ddedaa7fc9c3f4708af7431f5535666c174350830888c6b7/cryptography-50.0.1-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:51afcfceb15597cf2635068e4ac9a56b2abde622edde17f37d85fd7b5306497a", size = 4746230 },
{ url = "https://files.pythonhosted.org/packages/13/0e/b1f92e013228111413f2e6743948b80bc24dfd3c1b87ba98ceea16f5df89/cryptography-50.0.1-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:be224a65493ec5b74a158ff22a5522ce4a5ca1e543c647a3a4730d4a09e5f959", size = 4862596 },
{ url = "https://files.pythonhosted.org/packages/7e/22/c3654cccc856e9d682817b04ac3ee79731cb09ca6f95996a95c904de2883/cryptography-50.0.1-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9ebcdd5519be9b652a46f507817a74591774fc3d6923ac364e4dfa64e36b291b", size = 5014082 },
{ url = "https://files.pythonhosted.org/packages/42/8b/cb12b1b60c91b074ca6bf0fdd59aa8f10d8bc5f73af8faece86ef0421b37/cryptography-50.0.1-cp311-abi3-win_amd64.whl", hash = "sha256:aed8db4f6d71c51efb89530e12d9464e7bf2923d46c3205dc794a2a93f8c0648", size = 3842826 },
{ url = "https://files.pythonhosted.org/packages/5b/f0/424cb557d99aa86ac55da5e2add02e2882e44047b6264f93ade1b975a993/cryptography-50.0.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:30a125032e5642a21ff816e021152bd4e7e94f03eff3f4b7fca41cd22bc3110f", size = 3973525 },
{ url = "https://files.pythonhosted.org/packages/4d/72/3a2711d967977ab5fc80b782837c7e8d1ac7445e764c20c381a265c57ef3/cryptography-50.0.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a0b1a59e3a089064a0ec309e9428c8e3ae4e161419d20ac33600767e83fc658a", size = 4708817 },
{ url = "https://files.pythonhosted.org/packages/b4/f2/bb1f56e10815b789df0b409a69fa4992ff3d3fef9c72747f4a6b26fed38e/cryptography-50.0.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8921d58f426793c5f1b47f0b59575780de9a095214958d0eb37d909593db8367", size = 4697300 },
{ url = "https://files.pythonhosted.org/packages/08/bd/ed5396be499ffcf8807a585bfe38b71a1fbdd1c342b4f9b6d0ef5162a946/cryptography-50.0.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:a8f40ea47330e71b594a7e246898f93177c259490c63183dbaf9e571d71ed9a5", size = 4716039 },
{ url = "https://files.pythonhosted.org/packages/f6/6e/1cf405c5c8e8df7545378048e954792f00b7f2367af8863ce8b8f3e10607/cryptography-50.0.1-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:a255449073358275b64b67d3f595f268bbef70e72b6edb65e0c70c735bf739c9", size = 5332388 },
{ url = "https://files.pythonhosted.org/packages/47/92/b4317e8c32c4f47b062f5398bd79106b220a124546f42be83bf32b761e2a/cryptography-50.0.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:8df2de9102026855887e4587084f6eabd80ed0f345b8ad8a7ac27ab9bf4723e0", size = 4730293 },
{ url = "https://files.pythonhosted.org/packages/39/0d/a1e7633e2c744d0f2983320a27e924ef2264c79c56e1a58d5fb0a1cfd413/cryptography-50.0.1-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:ac02b07824d4d1001bd4367599f839c19cb171924c796e52c23508ac14c2c0cc", size = 4346031 },
{ url = "https://files.pythonhosted.org/packages/88/dd/b215616f9bab3fc18510c78a4e5c9f362d77838503c363dc747c7d4f5c6f/cryptography-50.0.1-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:cbf74a81765ee67413503ca6e26dcc4f6f5a519822436cc0a1b97aab6c1b8a17", size = 4715344 },
{ url = "https://files.pythonhosted.org/packages/b1/1b/ec3ebd31741d0e963612c4fe43caa39341b9b1e031e469820e42e4c83918/cryptography-50.0.1-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:16c5ecd954b3330ebfb6605eca4fd952da8bef376551d5cc264534e3770a9ee6", size = 5287201 },
{ url = "https://files.pythonhosted.org/packages/1a/01/0127d11a762b31a9ee0221894f540318761783f3fdc4bc5d057698caebd5/cryptography-50.0.1-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:79bf008d1f9af6071c797ad133e39915dfee7614f18f18f4db9072eb715064a3", size = 4730023 },
{ url = "https://files.pythonhosted.org/packages/9e/b9/e7425ebfb599241a0c1d7000f1b466c3062da66c19d9525031315dff7213/cryptography-50.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:330fbb252391c596f1ae42c5754449dc924e6ad012dca8efe0d703f9f2d12ec6", size = 4847362 },
{ url = "https://files.pythonhosted.org/packages/2d/fd/60d0ddf4defa12e482c9d5e0f554384d6e8ab25341fd15f060028fd92e6a/cryptography-50.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:42be3bb70596b3abe4ac097b75be223e8b3ab614a0e5de068e3dcc54d71d6149", size = 4999247 },
{ url = "https://files.pythonhosted.org/packages/4d/56/bc4f2b209e766c93372cfcd59b781a0b2b59700f62a969580415b699c2b2/cryptography-50.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:f74455bb086a85d5e81246412602aaa97ed095e504cd40dd261ef50be42205bf", size = 3825806 },
{ url = "https://files.pythonhosted.org/packages/84/a9/ee16a903f13755e914d1eecc482fe64d1f10761c3960e5d8fa6837377aff/cryptography-50.0.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ca83d00d9e69cd5eb63f2e69c3a5a59e0cecae5ae14c6ae0b35830fe3b37bad0", size = 4035307 },
{ url = "https://files.pythonhosted.org/packages/5e/a5/9ec7e81e8526c0d7a387d73386b2daed3f39e10d81a85930bd1b6bfba65c/cryptography-50.0.1-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:05ba322c4da95b262a212c345af888ef2c37c88c0509756ea00a0e6d68850f23", size = 4751900 },
{ url = "https://files.pythonhosted.org/packages/7e/3c/0e77bd5ffcf078e9dd27d3074aad6c030d9b10d0bf69329d573c927a188c/cryptography-50.0.1-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e22dfed744bd4002e909464cb23d2f0b05c6f3113a79ef2e9864a53db737c733", size = 4738357 },
{ url = "https://files.pythonhosted.org/packages/27/3a/3c5f80daa4dcd47323c7af8a2fcb90de27a33564d4fcac69846c0972691a/cryptography-50.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:4c4188f7c0cf655be5c06342b817ed0f9595b69ffa2b12026e5353eed29dea88", size = 4758474 },
{ url = "https://files.pythonhosted.org/packages/6e/2b/214cf0cf93db9628c3c20c896b229f327f6fb1b20e4b3743d8ad3f00af8b/cryptography-50.0.1-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:2ebbfb0f1fed745e91796e3e1080a1440423fdae8ece1b995a1d80883a409054", size = 5375862 },
{ url = "https://files.pythonhosted.org/packages/d6/51/3f9701867a46b6c1740c9b52fc4d3bed6cbdcfedcc9b6e64305c07f39cff/cryptography-50.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:407fe2b6db00939c05c0e945e9914238f2f0a430974839429dafc82b1ee6bee5", size = 4772942 },
{ url = "https://files.pythonhosted.org/packages/0d/5c/13ea642e08e2544d0f5396122055f4820cfacb3203562197b5967125ea97/cryptography-50.0.1-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:2b34d76a652ea2b6faf777c35df230c5637842cd904e04f16230c3f9f03e4361", size = 4383347 },
{ url = "https://files.pythonhosted.org/packages/84/d5/7d1fe1cb93f91c428093ff234e128c89ba8ea61a6f26aab406081f9b996e/cryptography-50.0.1-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:01f41478cf33fc605a6a089cd56d28b45c6c0b45a1928b61797f2621a04bac71", size = 4758050 },
{ url = "https://files.pythonhosted.org/packages/dd/04/557fc5ead96a829e0bc812a3b9dc4a52a2f27e4f7f5950da7ff27653a805/cryptography-50.0.1-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:fc3ed7ebd2a8c96f5b166de0ab9b624996bef3b07bbeb19364dfb78222c22c80", size = 5332955 },
{ url = "https://files.pythonhosted.org/packages/8c/eb/5d7124083e8d8cda8f5b348f544b71ad6f707ad63193758ef4d8e569da02/cryptography-50.0.1-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9dde0a357190eb3b1da1bb9ab750e9c85cba82ca5977aa0836cbb94e92611239", size = 4772694 },
{ url = "https://files.pythonhosted.org/packages/63/8e/f1f955e0921dd2b6d22eae7e8d24a4c4b638d10735ffbf6a71f99eb0fcb8/cryptography-50.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd3718b960d0b5dd213cdf03f3bcb7000e69dda0de8b956061947ff6bcff5558", size = 4888413 },
{ url = "https://files.pythonhosted.org/packages/1f/ab/89e2b798d2c3925f82e2bb72d5979f3d2f6da2dd22ef4a8cd8b70d920039/cryptography-50.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2a93d05e34d5f67fba6f891fe85d929999baa7195e853923ea6d7576c9e68c5e", size = 5044355 },
{ url = "https://files.pythonhosted.org/packages/99/89/87ef49ffe383ef4e147d27b7bf2088fb0b54ea409dd87b5a89442e5828a5/cryptography-50.0.1-cp39-abi3-win_amd64.whl", hash = "sha256:55d16b1ef3ee0958d893a977b19777887e546c9954ea81b200c3301a864013f2", size = 3875429 },
{ url = "https://files.pythonhosted.org/packages/c7/27/8d207af749c453ee17ea087340b3f2b4adef75aadd1d277b1b129bdda84e/cryptography-50.0.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9cb3cb952cf5a8abd50c782a98a89d71699715e802fe349704b47f2425b42a94", size = 3974350 },
{ url = "https://files.pythonhosted.org/packages/14/9a/6d3a4d7852e22d657438b7bf51f66102c7d71c0e1fafeec652281d0403e5/cryptography-50.0.1-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:5fe939deeb161024a6be98229c953b6591fef1f41214497a78fe793a244c017f", size = 4698675 },
{ url = "https://files.pythonhosted.org/packages/73/35/5c3717edf9e68a0550ce04e28eab493fe545eccd81742af03f6a75fe260b/cryptography-50.0.1-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fb4b9672d389c738b175c4166e78310f8a70358886aacd9173ee03a85ffdc671", size = 4707410 },
{ url = "https://files.pythonhosted.org/packages/1d/e0/e786934472e3ac4ecdecc7b129a0ca1a2a40dffdafcf2c3ea9d4397f8def/cryptography-50.0.1-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d63ae8f6481fec907ac0f588eee8a90aefde112c633131fe540e5711ddbb5a4e", size = 4698378 },
{ url = "https://files.pythonhosted.org/packages/51/cf/5b3f53a0b74d122f023476ede40ba5d3e70d5cf475f73b899740d26a4fb2/cryptography-50.0.1-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:804728ce710890870f3aaa344b2e161172d258d768ac139d02cfd9092d0d94e6", size = 4706889 },
{ url = "https://files.pythonhosted.org/packages/71/44/711e61f7d014be825ef79b285b047292d1bf893732ac1bc030a351fb517f/cryptography-50.0.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:693c99b49bd37d0d096e4334c10232c77248c415b98d35236094cdf96d57258b", size = 3824006 },
]
[[package]] [[package]]
name = "fastapi" name = "fastapi"
version = "0.135.1" version = "0.135.1"
@ -464,6 +620,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/57/bf/2086963c69bdac3d7cff1cc7ff79b8ce5ea0bec6797a017e1be338a46248/protobuf-6.33.5-py3-none-any.whl", hash = "sha256:69915a973dd0f60f31a08b8318b73eab2bd6a392c79184b3612226b0a3f8ec02", size = 170687 }, { url = "https://files.pythonhosted.org/packages/57/bf/2086963c69bdac3d7cff1cc7ff79b8ce5ea0bec6797a017e1be338a46248/protobuf-6.33.5-py3-none-any.whl", hash = "sha256:69915a973dd0f60f31a08b8318b73eab2bd6a392c79184b3612226b0a3f8ec02", size = 170687 },
] ]
[[package]]
name = "pycparser"
version = "3.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172 },
]
[[package]] [[package]]
name = "pydantic" name = "pydantic"
version = "2.12.5" version = "2.12.5"

View file

@ -150,3 +150,22 @@ scoped source/sync broker: ArgoCD Core offers no API-server token lane, and a
Kubernetes Application patch grant cannot restrict fields. Concrete enforcement Kubernetes Application patch grant cannot restrict fields. Concrete enforcement
contract is platform docs/activity-core-release-admission.md. Authenticated contract is platform docs/activity-core-release-admission.md. Authenticated
receipts and automatic rollback proof remain required; no broad token is admitted. receipts and automatic rollback proof remain required; no broad token is admitted.
## Broker core and isolated recovery proof — 2026-09-27
Implemented Ed25519 receipt verification with trusted role/principal bindings,
independent producer/reviewer keys, exact commit/manifest/image bindings, mandatory
CI contexts, freshness, 24-hour signed observation and live/rollback retention
coverage. Shared image policy remains the admission gate. Fixed mutation builders
restrict platform edits to the child revision and ArgoCD operations to selective
root/child sync without pruning. SQLite serializes releases and records signed
receipts plus transitions; publication intent precedes external calls. Isolated
fixtures prove restart, lost publication responses, failed-health rollback and
failed rollback holding the release lock. No production authority is inferred.
See docs/release-broker.md for implemented behavior and exact remaining work:
authenticated Git/ArgoCD adapter, custody/admission, real attestation issuers and
continuous observer, Temporal dispatch and isolated transport/rollback proof.
The broker is disabled by default and not connected to live credentials or a
production schedule. Current deployed revision and healthy-soak clock are unchanged.
T03 stays progress; the full unattended acceptance is not complete.