Implement signed release admission and durable rollback coordinator
Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a0e241-8285-7a63-8c0c-51c9cb824dc3
This commit is contained in:
parent
a122b1a5a4
commit
22935955cc
10 changed files with 810 additions and 59 deletions
|
|
@ -14,7 +14,7 @@ COPY .forgejo/workflows/image.yaml ./.forgejo/workflows/image.yaml
|
|||
COPY schemas/ ./schemas/
|
||||
COPY k8s/ ./k8s/
|
||||
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
|
||||
FROM python:3.12-slim@sha256:78387bc3881b8273120a12ebe6c1ab22b018ccc2c9adf565ae1ac9b536e184ea AS runtime
|
||||
|
|
|
|||
72
docs/release-broker.md
Normal file
72
docs/release-broker.md
Normal 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.
|
||||
|
|
@ -13,6 +13,7 @@ dependencies = [
|
|||
"nats-py>=2.7",
|
||||
"httpx>=0.27",
|
||||
"pyyaml>=6.0",
|
||||
"cryptography>=44.0",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
|
|
|
|||
|
|
@ -1,63 +1,19 @@
|
|||
"""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.
|
||||
"""
|
||||
"""Compatibility CLI for the shared image-only release admission policy."""
|
||||
import argparse
|
||||
import copy
|
||||
from datetime import datetime, timedelta, timezone
|
||||
import json
|
||||
from pathlib import Path
|
||||
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
|
||||
from activity_core.gitops_policy import validate
|
||||
|
||||
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']}
|
||||
|
||||
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}')
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("before", type=Path)
|
||||
parser.add_argument("after", type=Path)
|
||||
parser.add_argument("evidence", type=Path)
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
print(json.dumps(validate(list(yaml.safe_load_all(args.before.read_text())),
|
||||
list(yaml.safe_load_all(args.after.read_text())),
|
||||
json.loads(args.evidence.read_text()))))
|
||||
except (ValueError, KeyError, TypeError) as exc:
|
||||
raise SystemExit(f"refused: {exc}")
|
||||
|
|
|
|||
55
src/activity_core/gitops_policy.py
Normal file
55
src/activity_core/gitops_policy.py
Normal 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']}
|
||||
218
src/activity_core/release_broker.py
Normal file
218
src/activity_core/release_broker.py
Normal 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()
|
||||
63
src/activity_core/release_operations.py
Normal file
63
src/activity_core/release_operations.py
Normal 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)),
|
||||
]
|
||||
202
tests/test_release_broker.py
Normal file
202
tests/test_release_broker.py
Normal 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
165
uv.lock
generated
|
|
@ -8,6 +8,7 @@ source = { editable = "." }
|
|||
dependencies = [
|
||||
{ name = "alembic" },
|
||||
{ name = "asyncpg" },
|
||||
{ name = "cryptography" },
|
||||
{ name = "fastapi" },
|
||||
{ name = "httpx" },
|
||||
{ name = "nats-py" },
|
||||
|
|
@ -29,6 +30,7 @@ dev = [
|
|||
requires-dist = [
|
||||
{ name = "alembic", specifier = ">=1.14" },
|
||||
{ name = "asyncpg", specifier = ">=0.29" },
|
||||
{ name = "cryptography", specifier = ">=44.0" },
|
||||
{ name = "fastapi", specifier = ">=0.115" },
|
||||
{ name = "httpx", specifier = ">=0.27" },
|
||||
{ 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 },
|
||||
]
|
||||
|
||||
[[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]]
|
||||
name = "click"
|
||||
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 },
|
||||
]
|
||||
|
||||
[[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]]
|
||||
name = "fastapi"
|
||||
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 },
|
||||
]
|
||||
|
||||
[[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]]
|
||||
name = "pydantic"
|
||||
version = "2.12.5"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
contract is platform docs/activity-core-release-admission.md. Authenticated
|
||||
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.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue